Compare commits

...

364 Commits

Author SHA1 Message Date
Alistair Smith
6e3ee654f2 Merge remote-tracking branch 'origin' into ali/react 2025-11-04 07:54:17 -08:00
Alistair Smith
e306ac831e Merge branch 'main' into ali/react 2025-10-21 08:06:12 +09:00
Alistair Smith
ed1eb21093 Merge branch 'main' of github.com:oven-sh/bun into ali/react 2025-10-09 15:18:01 -07:00
Alistair Smith
4a4a37796b Merge branch 'main' into ali/react 2025-10-07 22:02:46 -07:00
Alistair Smith
7aef153f5d refactor: Remove experimental types and related references from the codebase 2025-10-07 18:17:00 -07:00
Alistair Smith
3cb64478d7 fix: Merge with 1.3 new Bun.serve types 2025-10-07 17:03:23 -07:00
Alistair Smith
7dfed6b986 Merge branch 'main' of github.com:oven-sh/bun into ali/react 2025-10-07 16:50:14 -07:00
Alistair Smith
5972cf24cb Merge remote-tracking branch 'origin/main' into ali/react 2025-10-06 16:24:19 -07:00
Alistair Smith
329c79364d Merge branch 'main' into ali/react 2025-10-06 16:22:38 -07:00
Alistair Smith
da5bf73494 clean up args 2025-10-03 16:53:59 -07:00
Alistair Smith
fbd58db004 changes 2025-10-03 16:11:37 -07:00
Alistair Smith
81999c26e6 extend the debug timeout a little 2025-10-03 15:05:50 -07:00
Alistair Smith
dde1f9e610 pin peechy 2025-10-03 14:26:50 -07:00
Alistair Smith
c9b5ba1c96 bump builtin react-refresh 2025-10-03 13:17:53 -07:00
Alistair Smith
74f4fcf2a6 Revert launch.json 2025-10-03 12:58:41 -07:00
Alistair Smith
031f12442d fix: We do require react/react-dom 2025-10-03 12:53:48 -07:00
autofix-ci[bot]
a20a718e48 [autofix.ci] apply automated fixes 2025-10-03 18:41:41 +00:00
Alistair Smith
33537b6ec1 Merge branch 'main' into ali/react 2025-10-03 11:38:33 -07:00
Alistair Smith
952436fc4c returnIfException() 2025-10-03 11:36:02 -07:00
Alistair Smith
31352bc646 fix test 2025-10-03 11:36:02 -07:00
Alistair Smith
ff84564c11 fix that 2025-10-03 11:36:02 -07:00
Alistair Smith
cc78a1bca1 fix: re-inject react-refresh in auto mode 2025-10-03 11:36:02 -07:00
Alistair Smith
3ca89abacf remove 2025-10-02 18:33:40 -07:00
Alistair Smith
2f02e4e31d Merge branch 'main' of github.com:oven-sh/bun into ali/react 2025-10-02 18:21:29 -07:00
Alistair Smith
efb508e2ae chore: Bump & pin React 2025-10-02 18:14:47 -07:00
Alistair Smith
86af3dd034 fix test 2025-10-02 17:33:33 -07:00
autofix-ci[bot]
a6d3808ad8 [autofix.ci] apply automated fixes 2025-10-02 22:54:04 +00:00
Alistair Smith
2153fe4163 Merge branch 'main' of github.com:oven-sh/bun into ali/react 2025-10-02 15:50:40 -07:00
Alistair Smith
6977b36215 change 2025-09-30 22:04:08 -07:00
Alistair Smith
ceaab9eda3 tests: Fix spacing in routes test expectation 2025-09-30 19:33:14 -07:00
Alistair Smith
f262e32368 tidy 2025-09-30 19:18:21 -07:00
Alistair Smith
69a76d44f9 types 2025-09-30 18:52:50 -07:00
Alistair Smith
fc9538baf1 move 2025-09-30 18:46:34 -07:00
Alistair Smith
72b7956385 remove old property 2025-09-30 18:13:46 -07:00
Alistair Smith
62b296bb43 message 2025-09-30 18:08:10 -07:00
Alistair Smith
cb14f70a43 remove dated .auto() implementation 2025-09-30 18:05:30 -07:00
Alistair Smith
38511375f8 unused for now 2025-09-30 17:50:14 -07:00
Alistair Smith
06cfe2ead1 bump react 2025-09-30 17:48:05 -07:00
Alistair Smith
4f219503fe fix production.test.ts resolving bun-framework-react 2025-09-30 17:41:22 -07:00
Alistair Smith
b2353d687e Merge branch 'main' of github.com:oven-sh/bun into ali/react 2025-09-30 17:25:53 -07:00
Alistair Smith
65e66099f7 fix: updated load expectation 2025-09-30 17:25:25 -07:00
Alistair Smith
c534f0caa0 Merge branch 'main' into ali/react 2025-09-30 15:21:39 -07:00
Alistair Smith
a873152aeb Revert bundle_v2.zig 2025-09-30 15:19:17 -07:00
Alistair Smith
a17eb07d48 revert some unnecessary changes 2025-09-30 15:15:39 -07:00
Alistair Smith
1381de4d18 Merge branch 'main' of github.com:oven-sh/bun into ali/react 2025-09-30 15:04:48 -07:00
Alistair Smith
8d8b037e94 rm 2025-09-29 16:16:22 -07:00
Alistair Smith
f43a175f72 use options 2025-09-29 16:07:49 -07:00
Alistair Smith
e1ad16f857 Merge branch 'zack/ssg-3' of github.com:oven-sh/bun into ali/react 2025-09-29 15:42:55 -07:00
Zack Radisic
661a246039 fix 2025-09-28 18:02:58 -07:00
Zack Radisic
d0fed20c89 Revert "fix leak"
This reverts commit 1c6165e68a.
2025-09-28 17:43:20 -07:00
Zack Radisic
1c6165e68a fix leak 2025-09-27 15:39:40 -07:00
Zack Radisic
daefbfb453 Merge branch 'main' into zack/ssg-3 2025-09-27 15:09:54 -07:00
Zack Radisic
971e4679cf Merge branch 'zack/ssg-3' of github.com:oven-sh/bun into zack/ssg-3 2025-09-27 14:57:39 -07:00
Zack Radisic
49a9dc7ddf fix banword 2025-09-27 14:55:12 -07:00
Zack Radisic
b5a5fea9ae fix 2025-09-27 00:55:48 -07:00
Zack Radisic
243a237a62 use jsc call conv 2025-09-27 00:29:20 -07:00
Zack Radisic
49cfda12a8 Merge branch 'zack/ssg-3' of https://github.com/oven-sh/bun into zack/ssg-3 2025-09-27 00:28:16 -07:00
Zack Radisic
ab579a3cc3 less confusing 2025-09-27 00:27:35 -07:00
Zack Radisic
8cd9b4eae6 update leaksan.supp 2025-09-27 00:18:34 -07:00
Zack Radisic
dd9860f501 Merge branch 'main' into zack/ssg-3 2025-09-26 23:50:10 -07:00
Zack Radisic
756e590782 fix ban words 2025-09-26 22:07:39 -07:00
Alistair Smith
c62613e765 better file hash 2025-09-26 19:50:25 -07:00
Alistair Smith
7f96bf8f13 Merge branch 'zack/ssg-3' of github.com:oven-sh/bun into ali/react 2025-09-26 19:31:00 -07:00
Alistair Smith
cd800b02f5 decrash 2025-09-26 19:30:53 -07:00
Zack Radisic
de999f78ab resolve more comments 2025-09-26 02:04:35 -07:00
Zack Radisic
24748104ce okie dokie 2025-09-26 01:37:55 -07:00
Zack Radisic
3fca3b97d9 resolve comments 2025-09-26 01:11:29 -07:00
Alistair Smith
4cec2ecdc6 changes 2025-09-26 00:09:23 -07:00
Zack Radisic
cdeb7bfb00 proper event loop stuff 2025-09-25 20:35:42 -07:00
Alistair Smith
98b24f5797 2025-09-25 20:05:45 -07:00
Alistair Smith
678843fb59 Merge branch 'zack/ssg-3' of github.com:oven-sh/bun into ali/react 2025-09-25 20:00:06 -07:00
Alistair Smith
7227745249 0.0.0-canary.7 2025-09-25 19:59:35 -07:00
Zack Radisic
472e2d379f fixes 2025-09-25 19:54:39 -07:00
autofix-ci[bot]
16360c9432 [autofix.ci] apply automated fixes 2025-09-26 02:46:56 +00:00
Alistair Smith
1b3d0d5c40 regen 2025-09-25 19:43:57 -07:00
Alistair Smith
799248bfb4 use vendored rsdb 2025-09-25 19:42:06 -07:00
Alistair Smith
a2689c03e9 change 2025-09-25 19:31:10 -07:00
Zack Radisic
2a7e2c9cf3 Remove unncessary dupe 2025-09-25 19:13:59 -07:00
Alistair Smith
4f0d2a5624 throw if no exports instead of silently failing 2025-09-25 18:40:49 -07:00
Alistair Smith
23230112b0 types 2025-09-25 18:31:29 -07:00
Alistair Smith
c2c2a1685a changes 2025-09-25 18:15:31 -07:00
Alistair Smith
09716704bb pageModule might not resolve 2025-09-25 17:46:18 -07:00
Alistair Smith
2d3223c5a6 Merge branch 'zack/ssg-3' of github.com:oven-sh/bun into ali/react 2025-09-25 17:34:56 -07:00
Alistair Smith
891ea726d6 changes 2025-09-25 17:29:52 -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
Jarred Sumner
7204820f19 Update BakeGlobalObject.cpp 2025-09-24 23:47:29 -07:00
Jarred Sumner
af3a1ffd46 Update BakeGlobalObject.cpp 2025-09-24 23:47:18 -07:00
Jarred Sumner
2ff068dad2 Update visitExpr.zig 2025-09-24 23:07:27 -07:00
Alistair Smith
927065238b allow warnings in bun install 2025-09-24 21:55:31 -07:00
Alistair Smith
fa727b22de remove 2025-09-24 21:45:23 -07:00
Alistair Smith
f20b0ced8e tidy bake harness 2025-09-24 21:41:34 -07:00
Alistair Smith
17fdb5bcdf move this 2025-09-24 21:37:25 -07:00
Alistair Smith
f65d89ff8b changes 2025-09-24 21:34:49 -07:00
Alistair Smith
c1931c11fe lots of Framework interface documentation 2025-09-24 20:24:06 -07:00
Alistair Smith
67d27499c3 canary.5 2025-09-24 19:49:34 -07:00
Alistair Smith
85db75611b canary.3 2025-09-24 19:38:24 -07:00
Alistair Smith
61519b320d reorganise 2025-09-24 19:20:30 -07:00
Alistair Smith
c129d683cd types & other changes 2025-09-24 19:08:31 -07:00
Alistair Smith
0b0ffbf250 Merge branch 'zack/ssg-3' into ali/react 2025-09-24 18:58:13 -07:00
Alistair Smith
c64dd684c8 vendor react-server-dom-bun 2025-09-24 18:55:19 -07:00
Alistair Smith
5aa5906ccf Merge branch 'main' into zack/ssg-3 2025-09-24 18:48:29 -07:00
Alistair Smith
c93d8cf12b changes 2025-09-24 13:53:46 -07:00
Alistair Smith
1a1091fd2c 2025-09-23 21:49:34 -07:00
Alistair Smith
bdf77f968c Merge branch 'zack/ssg-3' of github.com:oven-sh/bun into ali/react 2025-09-23 21:12:39 -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
Alistair Smith
5b4b99e2c4 Merge branch 'zack/ssg-3' into ali/react 2025-09-23 21:08:04 -07:00
Alistair Smith
da2be3f582 cleanup 2025-09-23 20:55:33 -07:00
Alistair Smith
7282e92e48 try fix ci for bun-framework-react 2025-09-23 20:40:03 -07:00
Alistair Smith
80c28b6280 change 2025-09-23 18:55:03 -07:00
Alistair Smith
e40238fdc2 instant crash 2025-09-23 18:27:05 -07:00
Alistair Smith
166e961202 listen for shell 2025-09-23 18:24:18 -07:00
Zack Radisic
58ecff4e0c Merge branch 'main' into zack/ssg-3 2025-09-23 18:06:43 -07:00
Alistair Smith
a548ae7038 remove default title 2025-09-23 17:54:59 -07:00
Alistair Smith
bbaabedce6 fix: use CatchScope 2025-09-23 16:55:29 -07:00
Alistair Smith
f84f90c09f rm .app test 2025-09-23 16:51:29 -07:00
Zack Radisic
43a7b6518a fix 2025-09-23 15:46:31 -07:00
Alistair Smith
c85ab5218e feat: report "use client" warning on more well known client hooks 2025-09-23 14:53:57 -07:00
Alistair Smith
bade403361 fix: Don't report more than once for missing "use client" calls 2025-09-23 14:45:10 -07:00
Alistair Smith
047eecc90c Merge branch 'zack/ssg-3' of github.com:oven-sh/bun into ali/react 2025-09-23 14:22: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
Alistair Smith
fc6fdbe300 rm 2025-09-22 23:20:56 -07:00
Alistair Smith
247629aded cleaning 2025-09-22 20:24:20 -07:00
Alistair Smith
2894e8d309 change 2025-09-22 20:03:16 -07:00
Alistair Smith
cc84e271ff changes 2025-09-22 18:10:57 -07:00
Alistair Smith
c07150d5b1 types 2025-09-22 16:27:10 -07:00
Alistair Smith
b0d3815cf9 Merge branch 'zack/ssg-3' of github.com:oven-sh/bun into ali/react 2025-09-22 16:24:39 -07:00
Zack Radisic
f145d8c30c Merge branch 'main' into zack/ssg-3 2025-09-22 15:59:55 -07:00
Alistair Smith
3a23965581 Merge remote-tracking branch 'origin/zack/ssg-3' into ali/react 2025-09-22 15:56:42 -07:00
Alistair Smith
0b45b9c29e Add request property to bun:app module and enhance type definitions
- Added `request` property to the `bun:app` module for better request handling.
- Updated `hmr-runtime-server.ts` to use `RouteMetadata` for improved type safety.
- Introduced `$ERR_SSR_RESPONSE_EXPECTED` error function in builtins for SSR response handling.
2025-09-22 15:49:29 -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
Alistair Smith
99a80a6fe6 changes 2025-09-19 18:47:08 -07:00
Alistair Smith
8b7bc0fe59 dont attempt to resolve initial rsc payload when doing first render on server 2025-09-19 18:38:53 -07:00
Alistair Smith
7e89ca3d2f link 2025-09-19 18:26:38 -07:00
Alistair Smith
d8fa01ed41 basic impl 2025-09-19 18:16:09 -07:00
Alistair Smith
361cd05676 move framework entrypoints 2025-09-19 18:09:14 -07:00
Zack Radisic
dbe15d3020 okay fuck 2025-09-19 17:44:08 -07:00
Alistair Smith
3a200e8097 overlay: print err message with whitespace formatting 2025-09-19 17:43:05 -07:00
Alistair Smith
2701292a9f edit react framework 2025-09-19 17:34:38 -07:00
Alistair Smith
61b1aded3e push 2025-09-19 17:27:14 -07:00
Alistair Smith
36a414c087 change import to type and add error handling for synthetic modules 2025-09-19 17:22:28 -07:00
Alistair Smith
ac02036879 Merge remote-tracking branch 'origin/zack/ssg-3' into ali/react 2025-09-19 17:01:07 -07:00
Alistair Smith
612d41185b change versions 2025-09-19 17:00:40 -07:00
Alistair Smith
59b34efea8 link 2025-09-19 16:58:51 -07:00
Alistair Smith
243f3652f1 remove old commented code 2025-09-19 16:52:03 -07:00
Alistair Smith
11dc2fae56 revert HotReloadEvent.zig 2025-09-19 16:36:54 -07:00
Alistair Smith
0919f237a5 remove 2025-09-19 16:13:59 -07:00
Alistair Smith
691e731404 changes 2025-09-19 16:12:32 -07:00
Alistair Smith
1a19be07ee remove unnecessary data output in development mode 2025-09-19 16:10:37 -07:00
Alistair Smith
903ac7bdd5 Refactor DevServer: remove unused bakeDebug output and clean up route bundling logic 2025-09-19 16:00:55 -07:00
Alistair Smith
0ac6b17d4a revert some changes 2025-09-19 15:53:49 -07:00
Alistair Smith
921e3578b1 change 2025-09-19 15:51:05 -07:00
Alistair Smith
101bcb1ea0 there is no is_built_in_react anymore 2025-09-19 15:45:11 -07:00
Alistair Smith
f691ea1e96 rm 2025-09-19 15:44:23 -07:00
Alistair Smith
53208e2538 .staticRouters is not implemented 2025-09-19 15:35:10 -07:00
Alistair Smith
53299d78b1 refactor framework definition and options interface in bun-types; remove package load logic from Framework struct 2025-09-19 15:20:34 -07:00
Alistair Smith
363c4a5c06 revert 2025-09-19 14:29:48 -07:00
Alistair Smith
6e6120640e remove react special case resolution 2025-09-19 14:29:02 -07:00
Alistair Smith
6556138c7b types 2025-09-19 14:08:00 -07:00
Alistair Smith
aea7b196e6 Revert visitExpr 2025-09-19 14:07:51 -07:00
Zack Radisic
b3f92b0889 fix test 2025-09-19 13:52:35 -07:00
Zack Radisic
dab797b834 fix test 2025-09-18 20:52:35 -07:00
Alistair Smith
a8ff3f8ac3 move module map 2025-09-18 18:58:12 -07:00
Alistair Smith
b516eedc67 fix mod 2025-09-18 17:57:46 -07:00
Alistair Smith
bcea163fd2 update framework def 2025-09-18 17:55:28 -07:00
Alistair Smith
a47cbef4ca restore bundler options 2025-09-18 17:39:48 -07:00
Alistair Smith
90e68fa095 use different react bundles 2025-09-18 17:33:49 -07:00
Alistair Smith
da0b090834 use the specific bundles 2025-09-18 17:25:26 -07:00
Alistair Smith
e6aced6637 remove 2025-09-18 17:17:27 -07:00
Alistair Smith
ce560cd318 Merge branch 'zack/ssg-3' of github.com:oven-sh/bun into ali/react 2025-09-18 17:16:07 -07:00
Alistair Smith
e554c4e1ca Update bun-framework-react dependencies and imports
- Changed the version of `react-server-dom-bun` from a local link to a specific experimental version in `package.json` and `bun.lock`.
- Updated the import path for `renderToPipeableStream` to use the `.node` variant for compatibility.
- Added `neo-async` as a dependency for `react-server-dom-bun` to ensure proper functionality.
2025-09-18 14:18:11 -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
Alistair Smith
c29c69b9b5 types 2025-09-17 13:55:46 -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
421a4f37cd FIX the concurrent request bug 2025-09-16 21:55:05 -07:00
Alistair Smith
d0da7076e6 types 2025-09-16 16:35:41 -07:00
Alistair Smith
ea78d564da bump react 2025-09-16 16:29:45 -07:00
Alistair Smith
6338d55f70 continue 2025-09-16 15:56:29 -07:00
Alistair Smith
d3bdc77274 types 2025-09-16 15:56:24 -07:00
Alistair Smith
ecd2fed665 try with redo of react-server-dom-bun 2025-09-16 15:11:22 -07:00
Alistair Smith
28447ab578 Refactor HotReloadEvent and comment out unused code in BundleV2
- Updated `HotReloadEvent.zig` to use duplicated strings for extra files to prevent potential memory issues.
- Commented out unused code related to "bun:app" in `bundle_v2.zig` to improve clarity and maintainability.
2025-09-15 21:55:59 -07:00
Alistair Smith
3e798f1787 type only import 2025-09-15 21:24:12 -07:00
Alistair Smith
a64f073ad3 minimize 2025-09-15 21:13:45 -07:00
Alistair Smith
bb19610f0d Update Bun framework to use react-server-dom-webpack and upgrade dependencies
- Changed module imports from "react-server-dom-bun" to "react-server-dom-webpack" in multiple files.
- Updated dependencies in package.json and bun.lock to use newer versions of React, React DOM, React Refresh, and the new react-server-dom-webpack package.
- Adjusted type definitions in bun-types to reflect changes in import sources.

This update enhances compatibility with the latest React features and improves the overall framework structure.
2025-09-15 21:00:46 -07:00
Alistair Smith
ed4a887047 Merge branch 'zack/ssg-3' into ali/react 2025-09-15 16:38:40 -07:00
Alistair Smith
894a654e26 print errors during framework resolution 2025-09-15 16:38:27 -07:00
Zack Radisic
99dd08bccb Merge branch 'main' into zack/ssg-3 2025-09-15 16:26:34 -07:00
Alistair Smith
7339d1841b specify absolute path 2025-09-15 15:15:52 -07:00
Alistair Smith
1217e87379 remove builtInModules in zig 2025-09-15 15:07:32 -07:00
Alistair Smith
704661e96f remove support for builtInModules 2025-09-15 15:05:32 -07:00
autofix-ci[bot]
8e659b2dc8 [autofix.ci] apply automated fixes 2025-09-15 20:44:31 +00:00
Alistair Smith
93007de396 Merge branch 'zack/ssg-3' of github.com:oven-sh/bun into ali/react 2025-09-15 13:39:44 -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
Alistair Smith
d4a9c7a161 some helpful logs 2025-09-12 18:38:12 -07:00
Alistair Smith
b5c16dcc1b Enhance DevServer logging for framework initialization and route scanning
This update adds detailed logging to the DevServer's initialization process, including framework configuration details such as server components, React Fast Refresh, and file system router types. Additionally, it logs the directory being scanned for routes and the number of routes found, improving visibility into the server's setup and routing process.
2025-09-12 17:49:45 -07:00
Alistair Smith
0ba166eea3 cleaning 2025-09-12 17:31:43 -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
Alistair Smith
2bd5d68047 Add support for --app flag in CLI to run bun.app.ts or bun.app.js
This update allows users to specify the `--app` flag when running Bun commands. If the flag is provided without additional arguments, the CLI will automatically look for and execute the bun.app.ts or bun.app.js file in the current working directory. If the file is not found, an error message will be displayed, guiding users to the expected file locations.
2025-09-12 16:16:17 -07:00
Zack Radisic
9c5c4edac4 Bun.SSRResponse -> import { Response } from 'bun:app' 2025-09-12 15:47:26 -07:00
Alistair Smith
cae0673dc4 load the framework 2025-09-12 14:44:51 -07:00
Alistair Smith
51e18d379f try this 2025-09-12 13:32:13 -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
Alistair Smith
d54ffd8012 dont use dom libs 2025-09-11 17:43:45 -07:00
Alistair Smith
dca34819b6 remove uninit state for a store 2025-09-11 16:45:55 -07:00
Alistair Smith
b4add533e6 fix a bunch of typescript problems in our test suite 2025-09-11 15:30:41 -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
Alistair Smith
a63888ed6d Improve type safety of DOM element creation utils
The improved types ensure only valid HTML attributes can be set for each
element type, making the code more type-safe and self-documenting.
2025-09-09 21:55:48 -07:00
Alistair Smith
e50385879b Fix Router navigation state handling
The changes improve state handling during client-side navigation by: -
Using correct function updates with setAppState - Preserving abort
controller on updates - Adding proper cache ID comparison - Moving
router instance to constants
2025-09-09 21:39:19 -07:00
Alistair Smith
20d2f3805e force passing a cacheId 2025-09-09 21:21:03 -07:00
Alistair Smith
d984f8f5ad Merge branch 'ali/react' of github.com:oven-sh/bun into ali/react 2025-09-09 21:09:58 -07:00
Alistair Smith
9ea2ec876e Refactor the client a lot 2025-09-09 21:06:37 -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
autofix-ci[bot]
02d9da73bd [autofix.ci] apply automated fixes 2025-09-10 02:43:59 +00:00
Alistair Smith
6562275d15 Merge remote-tracking branch 'origin/zack/ssg-3' into ali/react 2025-09-09 19:39:18 -07:00
Alistair Smith
cccae0cc79 Start splitting up the client 2025-09-09 19:37:58 -07:00
Zack Radisic
c10d184448 fix the test 2025-09-09 18:49:45 -07:00
Alistair Smith
0f8a232466 delete sources (now that they're gen'd on build) 2025-09-09 18:42:18 -07:00
autofix-ci[bot]
f47df15c18 [autofix.ci] apply automated fixes 2025-09-10 01:39:39 +00:00
Alistair Smith
f2d3141767 Merge remote-tracking branch 'origin/zack/ssg-3' into ali/react 2025-09-09 18:36:50 -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
Alistair Smith
151d8bb413 refine types for RSC payload handling in bun-framework-react 2025-09-08 19:51:33 -07:00
Alistair Smith
42bfccee3c revert buildstep changes 2025-09-08 19:40:19 -07:00
Alistair Smith
f219a29248 enqueueChunks 2025-09-08 19:28:45 -07:00
Alistair Smith
b588512237 change build 2025-09-08 18:53:39 -07:00
Alistair Smith
3a42ad8b1f make the build simpelr 2025-09-08 18:20:22 -07:00
Alistair Smith
cc1fff363d types 2025-09-08 18:02:17 -07:00
Alistair Smith
ba5e4784aa rm 2025-09-08 17:57:52 -07:00
Alistair Smith
3e747886aa typescript casting 2025-09-08 17:42:31 -07:00
Zack Radisic
9504d14b7a cache the wrap component function 2025-09-08 17:38:38 -07:00
Alistair Smith
911b670621 move types 2025-09-08 17:38:06 -07:00
Alistair Smith
679282b8c6 types 2025-09-08 17:32:50 -07:00
autofix-ci[bot]
1f79bc15a3 [autofix.ci] apply automated fixes 2025-09-09 00:24:14 +00:00
Alistair Smith
80a945f03f fix that 2025-09-08 17:22:06 -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
Alistair Smith
6b2c3e61ea refactor: update bun-framework-react structure and types
- Remove unused components and utility files from bun-framework-react.
- Update type definitions in bake.private.d.ts to allow for synthetic modules.
- Modify exports in package.json to simplify module access.
- Ensure proper registration of bun:app in the HMR module system.
2025-09-08 16:57:53 -07:00
Alistair Smith
43d447f9fe register bun:app properly 2025-09-08 16:19:50 -07:00
Zack Radisic
8b35b5634a Update stuff 2025-09-08 15:56:21 -07:00
Alistair Smith
811f0888c8 - Change unique symbol types to unknown in bake.private.d.ts for better type flexibility.
- Update onServerSideReload function return type in rendering.d.ts to void.
- Modify onServerSideReload declaration in hmr-module.ts to accept both Promise<void> and void.
- Enhance type safety in registerSynthetic function with a generic interface for built-in modules.
2025-09-08 15:28:51 -07:00
Alistair Smith
a5d7830862 build: add bun-framework-react to cmake source tracking
- Include packages/bun-framework-react in build dependencies
  - Update Sources.json to glob React framework source files
  - Fix TypeScript types in bun-framework-react (Uint8Array generics, assertions)
  - Add package.json exports for React framework modules
2025-09-08 15:18:10 -07:00
Alistair Smith
ef17dc57e4 refactor: update types in bake.private.d.ts and tsconfig.json; remove unused client and server rendering files 2025-09-08 15:08:29 -07:00
Alistair Smith
e58cb4511e changes to overlay and client.tsx entry 2025-09-08 15:02:56 -07:00
Alistair Smith
d44b3db1cb refactor: update server and SSR manifest types in hmr-module.ts 2025-09-08 14:45:38 -07:00
alii
857e25d88c bun scripts/glob-sources.mjs 2025-09-08 21:41:31 +00:00
Alistair Smith
6eee2eeaf6 some ssr changes 2025-09-08 14:40:46 -07:00
Alistair Smith
cc3e4d8319 types changes 2025-09-08 14:24:40 -07:00
Alistair Smith
278c2e7fb6 fix relative path in bake.zig 2025-09-08 14:14:28 -07:00
Alistair Smith
e296928ab9 abortcontroller bun-types tests 2025-09-08 14:14:28 -07:00
Alistair Smith
39c1bf38f5 fix: AbortController missing a definition 2025-09-08 14:14:28 -07:00
Alistair Smith
91d30b4da0 update bun-types from main 2025-09-08 14:14:28 -07:00
autofix-ci[bot]
c2812fff79 [autofix.ci] apply automated fixes 2025-09-08 21:12:05 +00:00
Alistair Smith
e0337f5649 fix types for rendering api 2025-09-08 14:08:23 -07:00
alii
d05768cc18 bun scripts/glob-sources.mjs 2025-09-08 20:58:26 +00:00
Alistair Smith
1ad67908fc nuke bun-react 2025-09-08 13:58:05 -07:00
Alistair Smith
f55e320f41 start extracting parts of the framework out 2025-09-08 13:56:42 -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
103 changed files with 44794 additions and 2005 deletions

1
.gitattributes vendored
View File

@@ -47,6 +47,7 @@ examples/**/* linguist-documentation
vendor/*.c linguist-vendored
vendor/brotli/** linguist-vendored
packages/bun-framework-react/vendor/** linguist-vendored -diff -merge
test/js/node/test/fixtures linguist-vendored
test/js/node/test/common linguist-vendored

View File

@@ -41,6 +41,7 @@
},
"overrides": {
"@types/bun": "workspace:packages/@types/bun",
"@types/node": "24.3.1",
"bun-types": "workspace:packages/bun-types",
},
"packages": {
@@ -160,7 +161,7 @@
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
"@types/node": ["@types/node@24.2.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ=="],
"@types/node": ["@types/node@24.3.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g=="],
"@types/react": ["@types/react@19.1.10", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-EhBeSYX0Y6ye8pNebpKrwFJq7BoQ8J5SO6NlvNwwHjSj6adXJViPQrKlsyPw7hLBLvckEMO1yxeGdR82YBBlDg=="],

View File

@@ -27,6 +27,10 @@
"paths": ["src/bake/*.ts", "src/bake/*/*.{ts,css}"],
"exclude": ["src/bake/generated.ts"]
},
{
"output": "BunFrameworkReactSources.txt",
"paths": ["packages/bun-framework-react/*.{ts,tsx,js,jsx}", "packages/bun-framework-react/src/**/*.{ts,tsx,js,jsx}"]
},
{
"output": "BindgenSources.txt",
"paths": ["src/**/*.bind.ts"]

View File

@@ -23,7 +23,8 @@
},
"resolutions": {
"bun-types": "workspace:packages/bun-types",
"@types/bun": "workspace:packages/@types/bun"
"@types/bun": "workspace:packages/@types/bun",
"@types/node": "24.3.1"
},
"scripts": {
"build": "bun --silent run build:debug",

View File

@@ -0,0 +1 @@
bun-framework-react*.tgz

View File

@@ -0,0 +1,10 @@
<img src="https://bun.com/logo.png" height="36" />
# `bun-framework-react`
An implementation of the Bun Rendering API for React, with RSC (React Server Components)
1. `bun add bun-framework-react --dev`
2. Make a `pages/index.tsx` file, with a page component defualt export
3. Run `bun --app`
4. Open [localhost:3000](https://localhost:3000) 🎉

View File

@@ -0,0 +1,32 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "bun-framework-react",
"dependencies": {
"react": "0.0.0-experimental-a757cb76-20251002",
"react-dom": "0.0.0-experimental-a757cb76-20251002",
"react-refresh": "0.0.0-experimental-a757cb76-20251002",
},
"devDependencies": {
"@types/react": "^19.1.13",
"@types/react-dom": "^19.1.9",
},
},
},
"packages": {
"@types/react": ["@types/react@19.1.13", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ=="],
"@types/react-dom": ["@types/react-dom@19.1.9", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ=="],
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"react": ["react@0.0.0-experimental-a757cb76-20251002", "", {}, "sha512-7ZcE4sSUGgrXgUWa84iwC9DqwDFbQBgffFmu2DoNqFseruA/JjxQDXKwpV5acdxOM/0uzfSGrapHU3C3ZLiU2g=="],
"react-dom": ["react-dom@0.0.0-experimental-a757cb76-20251002", "", { "dependencies": { "scheduler": "0.0.0-experimental-a757cb76-20251002" }, "peerDependencies": { "react": "0.0.0-experimental-a757cb76-20251002" } }, "sha512-XjIkmW8mMx9kURHJUY+dhv1Ugan3RmEJIwrZEbAFcJA4S8RXL1wl+xsQJpDCh8kmeX/n25VAmFY8/j1MzqUHfA=="],
"react-refresh": ["react-refresh@0.0.0-experimental-a757cb76-20251002", "", {}, "sha512-uYd+N2W8/LymZQyY5u1BMWVvLlBV+5SxztBsFjOGuitE4x7sSCj8TwgS+8bxIEBucEVJglfOhDPCPohL/uEQdg=="],
"scheduler": ["scheduler@0.0.0-experimental-a757cb76-20251002", "", {}, "sha512-YCVGuzmF7u5HIpOdPFD4tZTPzQlOrtViag7uaWjJXfFx37C8sypfNeSXNXoYJeT/ICybxP1EsbHh2oByQHC2Cg=="],
}
}

View File

@@ -0,0 +1,64 @@
import { onServerSideReload } from "bun:app/client";
import { hydrateRoot } from "react-dom/client";
import { initialRscPayloadThen } from "./src/client/app.ts";
import { router } from "./src/client/constants.ts";
import { Root } from "./src/client/root.tsx";
hydrateRoot(document, <Root />, {
onUncaughtError(e) {
console.error(e);
},
});
const firstPageId = Date.now();
{
history.replaceState(firstPageId, "", location.href);
initialRscPayloadThen(result => {
if (router.hasNavigatedSinceDOMContentLoaded()) return;
// Collect the list of CSS files that were added from SSR
const links = document.querySelectorAll<HTMLLinkElement>("link[data-bake-ssr]");
router.css.clear();
for (let i = 0; i < links.length; i++) {
const link = links[i];
if (!link) continue;
const href = new URL(link.href).pathname;
router.css.push(href);
// Hack: cannot add this to `cssFiles` because React owns the element, and
// it will be removed when any navigation is performed.
}
router.setCachedPage(firstPageId, {
css: [...router.css.getList()],
element: result,
});
});
if (document.startViewTransition !== undefined) {
// View transitions are used by navigations to ensure that the page rerender
// all happens in one operation. Additionally, developers may animate
// different elements. The default fade animation is disabled so that the
// out-of-the-box experience feels like there are no view transitions.
// This is done client-side because a React error will unmount all elements.
const sheet = new CSSStyleSheet();
document.adoptedStyleSheets.push(sheet);
sheet.replaceSync(":where(*)::view-transition-group(root){animation:none}");
}
}
window.addEventListener("popstate", async event => {
const state = typeof event.state === "number" ? event.state : undefined;
await router.navigate(location.href, state);
});
if (import.meta.env.DEV) {
// Frameworks can call `onServerSideReload` to hook into server-side hot
// module reloading.
onServerSideReload(async () => {
const newId = Date.now();
history.replaceState(newId, "", location.href);
await router.navigate(location.href, newId);
});
}

View File

@@ -0,0 +1,35 @@
import type { Framework } from "bun:app";
function resolve(specifier: string) {
return Bun.fileURLToPath(import.meta.resolve(specifier));
}
const framework: Framework = {
serverComponents: {
separateSSRGraph: true,
serverRuntimeImportSource: resolve("./vendor/react-server-dom-bun/server.node.js"),
},
reactFastRefresh: {
importSource: resolve("react-refresh/runtime"),
},
fileSystemRouterTypes: [
{
root: "pages",
clientEntryPoint: resolve("./client.tsx"),
serverEntryPoint: resolve("./server.tsx"),
extensions: [".tsx", ".jsx"],
style: "nextjs-pages",
layouts: true,
ignoreUnderscores: true,
prefix: "/",
ignoreDirs: ["node_modules", ".git"],
},
],
// bundlerOptions: {
// ssr: {
// conditions: ["react-server"],
// },
// },
};
export default framework;

View File

@@ -0,0 +1,36 @@
{
"name": "bun-framework-react",
"version": "0.0.0-canary.10",
"devDependencies": {
"@types/react": "^19.1.13",
"@types/react-dom": "^19.1.9"
},
"exports": {
".": "./index.ts",
"./package.json": "./package.json",
"./ssr.tsx": "./ssr.tsx",
"./server.tsx": "./server.tsx",
"./client.tsx": "./client.tsx",
"./*": "./src/components/*.tsx"
},
"description": "React framework integration with RSC, for the Bun Rendering API",
"files": [
"./vendor",
"./src",
"./*.ts",
"./*.tsx",
"./README.md"
],
"keywords": [
"bun",
"react",
"framework",
"bake"
],
"type": "module",
"dependencies": {
"react": "0.0.0-experimental-a757cb76-20251002",
"react-dom": "0.0.0-experimental-a757cb76-20251002",
"react-refresh": "0.0.0-experimental-a757cb76-20251002"
}
}

View File

@@ -1,23 +1,24 @@
import type { Bake } from "bun";
import { renderToHtml, renderToStaticHtml } from "bun-framework-react/ssr.tsx" with { bunBakeGraph: "ssr" };
import { serverManifest } from "bun:bake/server";
import * as Bake from "bun:app";
import { serverManifest } from "bun:app/server";
import type { AsyncLocalStorage } from "node:async_hooks";
import { PassThrough } from "node:stream";
import { renderToPipeableStream } from "react-server-dom-bun/server.node.unbundled.js";
import type { RequestContext } from "../hmr-runtime-server";
import type { RequestContext } from "../../src/bake/hmr-runtime-server.ts";
import { renderToPipeableStream } from "./vendor/react-server-dom-bun/server.node.unbundled.js";
function assertReactComponent(Component: any) {
function assertReactComponent(Component: unknown): asserts Component is React.JSXElementConstructor<unknown> {
if (typeof Component !== "function") {
console.log("Expected a React component", Component, typeof Component);
throw new Error("Expected a React component");
}
}
// This function converts the route information into a React component tree.
function getPage(meta: Bake.RouteMetadata & { request?: Request }, styles: readonly string[]) {
function getPage(meta: Bake.RouteMetadata & { request?: Request | undefined }, styles: readonly string[]) {
let route = component(meta.pageModule, meta.params, meta.request);
for (const layout of meta.layouts) {
const Layout = layout.default;
const Layout = layout.default as typeof layout.default & { displayName?: string };
Layout.displayName ??= "Layout";
if (import.meta.env.DEV) assertReactComponent(Layout);
route = <Layout params={meta.params}>{route}</Layout>;
}
@@ -26,7 +27,6 @@ function getPage(meta: Bake.RouteMetadata & { request?: Request }, styles: reado
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Bun + React Server Components</title>
{styles.map(url => (
// `data-bake-ssr` is used on the client-side to construct the styles array.
<link key={url} rel="stylesheet" href={url} data-bake-ssr />
@@ -37,8 +37,12 @@ function getPage(meta: Bake.RouteMetadata & { request?: Request }, styles: reado
);
}
function component(mod: any, params: Record<string, string> | null, request?: Request) {
function component(mod: any, params: Record<string, string | string[]> | null, request?: Request) {
if (!mod || !mod.default) {
throw new Error("Pages must have a default export that is a React component");
}
const Page = mod.default;
let props = {};
if (import.meta.env.DEV) assertReactComponent(Page);
@@ -51,7 +55,6 @@ function component(mod: any, params: Record<string, string> | null, request?: Re
props = method();
}
// Pass request prop if mode is 'ssr'
if (mod.mode === "ssr" && request) {
props.request = request;
}
@@ -76,7 +79,7 @@ export async function render(
const skipSSR = request.headers.get("Accept")?.includes("text/x-component");
// Check if the page module has a streaming export, default to false
const streaming = meta.pageModule.streaming ?? false;
const streaming = meta.pageModule?.streaming ?? false;
// Do not render <link> tags if the request is skipping SSR.
const page = getPage(meta, skipSSR ? [] : meta.styles);
@@ -104,7 +107,6 @@ export async function render(
// Mark as aborted and call the abort function
signal.aborted = err;
// @ts-expect-error
signal.abort(err);
rscPayload.destroy(err);
},
@@ -236,5 +238,5 @@ export const contentTypeToStaticFile = {
export interface MiniAbortSignal {
aborted: Error | undefined;
/** Caller must set `aborted` to true before calling. */
abort: () => void;
abort: (reason?: any) => void;
}

View File

@@ -0,0 +1,96 @@
import type { ReactNode, SetStateAction } from "react";
import { createFromReadableStream } from "../../vendor/react-server-dom-bun/client.browser.js";
import { store, useStore, type Store } from "./store.ts";
export type NonNullishReactNode = Exclude<ReactNode, null | undefined>;
export type RenderableRscPayload = Promise<NonNullishReactNode> | NonNullishReactNode;
const encoder = new TextEncoder();
function enqueueChunks(
controller: ReadableStreamDefaultController<Uint8Array<ArrayBuffer>>,
...chunks: (string | Uint8Array<ArrayBuffer>)[]
) {
for (let chunk of chunks) {
if (typeof chunk === "string") {
chunk = encoder.encode(chunk);
}
controller.enqueue(chunk);
}
}
export interface AppState {
/**
* The renderable RSC payload
*/
rsc: RenderableRscPayload;
/**
* A controller that aborts on the first render
*/
abortOnRender?: AbortController | undefined;
}
// The initial RSC payload is put into inline <script> tags that follow the pattern
// `(self.__bun_f ??= []).push(chunk)`, which is converted into a ReadableStream
// here for React hydration. Since inline scripts are executed immediately, and
// this file is loaded asynchronously, the `__bun_f` becomes a clever way to
// stream the arbitrary data while HTML is loading. In a static build, this is
// setup as an array with one string.
const initialRscPayload: Promise<NonNullishReactNode> =
typeof document === "undefined"
? Promise.resolve(false)
: createFromReadableStream(
new ReadableStream<NonNullishReactNode>({
start(controller) {
const bunF = (self.__bun_f ??= []);
const originalPush = bunF.push;
bunF.push = function (this: typeof bunF, ...chunks: (string | Uint8Array<ArrayBuffer>)[]) {
enqueueChunks(controller, ...chunks);
return originalPush.apply(this, chunks);
}.bind(bunF);
bunF.forEach(chunk => enqueueChunks(controller, chunk));
if (document.readyState === "loading") {
document.addEventListener(
"DOMContentLoaded",
() => {
controller.close();
},
{ once: true },
);
} else {
controller.close();
}
},
}),
);
declare global {
interface Window {
__bun_f: Array<string | Uint8Array<ArrayBuffer>>;
}
}
const appStore: Store<AppState> = store<AppState>({
rsc: initialRscPayload,
});
export function setAppState(element: SetStateAction<AppState>): void {
appStore.write(element);
}
export function useAppState(): AppState {
return useStore(appStore);
}
export function getAppState(): AppState {
return appStore.read();
}
export function initialRscPayloadThen(then: (rsc: NonNullishReactNode) => void): void {
void initialRscPayload.then(then);
}

View File

@@ -0,0 +1,3 @@
import { Router } from "./router.ts";
export const router: Router = new Router();

View File

@@ -0,0 +1,355 @@
export class BakeCSSManager {
private readonly td = new TextDecoder();
// It is the framework's responsibility to ensure that client-side navigation
// loads CSS files. The implementation here loads all CSS files as <link> tags,
// and uses the ".disabled" property to enable/disable them.
private readonly cssFiles = new Map<string, { promise: Promise<void> | null; link: HTMLLinkElement }>();
private currentCssList: string[] | null = null;
public async set(list: string[]): Promise<void> {
this.currentCssList = list;
await this.ensureCssIsReady(this.currentCssList);
}
/**
* Get the actual list instance. Mutating this list will update the current
* CSS list (it is the actual array).
*/
public getList(): string[] {
return (this.currentCssList ??= []);
}
public clear(): void {
this.currentCssList = [];
}
public push(href: string): void {
const arr = this.getList();
arr.push(href);
}
/** This function blocks until all CSS files are loaded. */
ensureCssIsReady(cssList: string[] = this.currentCssList ?? []): Promise<void[]> | void {
const wait: Promise<void>[] = [];
for (const href of cssList) {
const existing = this.cssFiles.get(href);
if (existing) {
const { promise, link } = existing;
if (promise) {
wait.push(promise);
}
link.disabled = false;
} else {
const link = document.createElement("link");
let entry: { promise: Promise<void> | null; link: HTMLLinkElement };
const promise = new Promise<void>((resolve, reject) => {
link.rel = "stylesheet";
link.onload = resolve.bind(null, undefined);
link.onerror = reject;
link.href = href;
document.head.appendChild(link);
}).finally(() => {
entry.promise = null;
});
entry = { promise, link };
this.cssFiles.set(href, entry);
wait.push(promise);
}
}
if (wait.length === 0) {
return;
}
return Promise.all(wait);
}
public disableUnusedCssFilesIfNeeded(): void {
if (this.currentCssList) {
this.disableUnusedCssFiles();
}
}
disableUnusedCssFiles(): void {
// TODO: create a list of files that should be updated instead of a full loop
for (const [href, { link }] of this.cssFiles) {
if (!this.currentCssList!.includes(href)) {
link.disabled = true;
}
}
}
async readCssMetadata(
stream: ReadableStream<Uint8Array<ArrayBuffer>>,
): Promise<ReadableStream<Uint8Array<ArrayBuffer>>> {
let reader: ReadableStreamBYOBReader;
try {
// Using BYOB reader allows reading an exact amount of bytes, which allows
// passing the stream to react without creating a wrapped stream.
reader = stream.getReader({ mode: "byob" });
} catch (e) {
return this.readCssMetadataFallback(stream);
}
const header = (await reader.read(new Uint32Array(1))).value;
if (!header) {
if (import.meta.env.DEV) {
throw new Error("Did not read all bytes! This is a bug in bun-framework-react");
} else {
location.reload();
}
}
const first = header?.[0];
if (first !== undefined && first > 0) {
const cssRaw = (await reader.read(new Uint8Array(first))).value;
if (!cssRaw) {
if (import.meta.env.DEV) {
throw new Error("Did not read all bytes! This is a bug in bun-framework-react");
} else {
location.reload();
}
}
this.set(this.td.decode(cssRaw).split("\n"));
} else {
this.clear();
}
reader.releaseLock();
return stream;
}
/**
* Like readCssMetadata, but does NOT mutate the current CSS list. It returns
* the remaining stream after consuming the CSS header and the parsed list of
* CSS hrefs so callers can preload styles without switching the active list.
*/
async readCssMetadataForPrefetch(
stream: ReadableStream<Uint8Array<ArrayBuffer>>,
): Promise<{ stream: ReadableStream<Uint8Array<ArrayBuffer>>; list: string[] }> {
let reader: ReadableStreamBYOBReader;
try {
reader = stream.getReader({ mode: "byob" });
} catch (e) {
const s = await this.readCssMetadataFallbackForPrefetch(stream);
return { stream: s.stream, list: s.list };
}
const header = (await reader.read(new Uint32Array(1))).value;
if (!header) {
if (import.meta.env.DEV) {
throw new Error("Did not read all bytes! This is a bug in bun-framework-react");
} else {
location.reload();
}
}
const first = header?.[0];
let list: string[] = [];
if (first !== undefined && first > 0) {
const cssRaw = (await reader.read(new Uint8Array(first))).value;
if (!cssRaw) {
if (import.meta.env.DEV) {
throw new Error("Did not read all bytes! This is a bug in bun-framework-react");
} else {
location.reload();
}
}
list = this.td.decode(cssRaw).split("\n");
}
reader.releaseLock();
return { stream, list };
}
// Prefetch fallback variant that does not mutate currentCssList.
async readCssMetadataFallbackForPrefetch(
stream: ReadableStream<Uint8Array<ArrayBuffer>>,
): Promise<{ stream: ReadableStream<Uint8Array<ArrayBuffer>>; list: string[] }> {
const reader = stream.getReader();
const chunks: Uint8Array<ArrayBuffer>[] = [];
let totalBytes = 0;
const readChunk = async (size: number) => {
while (totalBytes < size) {
const { value, done } = await reader.read();
if (!done) {
chunks.push(value);
totalBytes += value.byteLength;
} else if (totalBytes < size) {
if (import.meta.env.DEV) {
throw new Error("Not enough bytes, expected " + size + " but got " + totalBytes);
} else {
location.reload();
}
}
}
if (chunks.length === 1) {
const first = chunks[0]!;
if (first.byteLength >= size) {
chunks[0] = first.subarray(size);
totalBytes -= size;
return first.subarray(0, size);
} else {
chunks.length = 0;
totalBytes = 0;
return first;
}
} else {
const buffer = new Uint8Array(size);
let i = 0;
let chunk: Uint8Array<ArrayBuffer> | undefined;
let len;
while (size > 0) {
chunk = chunks.shift();
if (!chunk) continue;
const { byteLength } = chunk;
len = Math.min(byteLength, size);
buffer.set(len === byteLength ? chunk : chunk.subarray(0, len), i);
i += len;
size -= len;
}
if (chunk !== undefined && len !== undefined && chunk.byteLength > len) {
chunks.unshift(chunk.subarray(len));
}
totalBytes -= size;
return buffer;
}
};
const header = new Uint32Array(await readChunk(4))[0];
let list: string[] = [];
if (header === 0) {
list = [];
} else if (header !== undefined) {
list = this.td.decode(await readChunk(header)).split("\n");
}
if (chunks.length === 0) {
return { stream, list };
}
// New readable stream that includes the remaining data
const remainingStream = new ReadableStream<Uint8Array<ArrayBuffer>>({
async start(controller) {
for (const chunk of chunks) {
controller.enqueue(chunk);
}
while (true) {
const { value, done } = await reader.read();
if (done) {
controller.close();
return;
}
controller.enqueue(value);
}
},
cancel() {
reader.cancel();
},
});
return { stream: remainingStream, list };
}
// Safari does not support BYOB reader. When this is resolved, this fallback
// should be kept for a few years since Safari on iOS is versioned to the OS.
// https://bugs.webkit.org/show_bug.cgi?id=283065
async readCssMetadataFallback(
stream: ReadableStream<Uint8Array<ArrayBuffer>>,
): Promise<ReadableStream<Uint8Array<ArrayBuffer>>> {
const reader = stream.getReader();
const chunks: Uint8Array<ArrayBuffer>[] = [];
let totalBytes = 0;
const readChunk = async (size: number) => {
while (totalBytes < size) {
const { value, done } = await reader.read();
if (!done) {
chunks.push(value);
totalBytes += value.byteLength;
} else if (totalBytes < size) {
if (import.meta.env.DEV) {
throw new Error("Not enough bytes, expected " + size + " but got " + totalBytes);
} else {
location.reload();
}
}
}
if (chunks.length === 1) {
const first = chunks[0]!;
if (first.byteLength >= size) {
chunks[0] = first.subarray(size);
totalBytes -= size;
return first.subarray(0, size);
} else {
chunks.length = 0;
totalBytes = 0;
return first;
}
} else {
const buffer = new Uint8Array(size);
let i = 0;
let chunk: Uint8Array<ArrayBuffer> | undefined;
let len;
while (size > 0) {
chunk = chunks.shift();
if (!chunk) continue;
const { byteLength } = chunk;
len = Math.min(byteLength, size);
buffer.set(len === byteLength ? chunk : chunk.subarray(0, len), i);
i += len;
size -= len;
}
if (chunk !== undefined && len !== undefined && chunk.byteLength > len) {
chunks.unshift(chunk.subarray(len));
}
totalBytes -= size;
return buffer;
}
};
const header = new Uint32Array(await readChunk(4))[0];
if (header === 0) {
this.clear();
} else if (header !== undefined) {
this.set(this.td.decode(await readChunk(header)).split("\n"));
}
if (chunks.length === 0) {
return stream;
}
// New readable stream that includes the remaining data
return new ReadableStream<Uint8Array<ArrayBuffer>>({
async start(controller) {
for (const chunk of chunks) {
controller.enqueue(chunk);
}
while (true) {
const { value, done } = await reader.read();
if (done) {
controller.close();
return;
}
controller.enqueue(value);
}
},
cancel() {
reader.cancel();
},
});
}
}

View File

@@ -0,0 +1,3 @@
export function isThenable<T>(payload: PromiseLike<T> | unknown): payload is PromiseLike<T> {
return payload !== null && typeof payload === "object" && "then" in payload;
}

View File

@@ -0,0 +1,29 @@
import { use, useLayoutEffect, type ReactNode } from "react";
import { useAppState } from "./app.ts";
import { router } from "./constants.ts";
import { isThenable } from "./lib/util.ts";
// This is a function component that uses the `use` hook, which unwraps a
// promise. The promise results in a component containing suspense boundaries.
// This is the same logic that happens on the server, except there is also a
// hook to update the promise when the client navigates. The `Root` component
// also updates CSS files when navigating between routes.
export function Root(): ReactNode {
const app = useAppState();
// Layout effects are executed right before the browser paints,
// which is the perfect time to make CSS visible.
useLayoutEffect(() => {
if (app.abortOnRender) {
try {
app.abortOnRender.abort();
} catch {}
}
requestAnimationFrame(() => {
router.css.disableUnusedCssFilesIfNeeded();
});
});
return isThenable(app.rsc) ? use(app.rsc) : app.rsc;
}

View File

@@ -0,0 +1,215 @@
import { flushSync } from "react-dom";
import { createFromReadableStream } from "../../vendor/react-server-dom-bun/client.browser.js";
import { getAppState, setAppState, type AppState, type NonNullishReactNode } from "./app.ts";
import { BakeCSSManager } from "./css.ts";
export interface CachedPage {
css: string[];
element: NonNullishReactNode;
}
export class Router {
private lastNavigationId: number = 0;
private lastNavigationController: AbortController | null = null;
// Keep a cache of page objects to avoid re-fetching a page when pressing the
// back button. The cache is indexed by the date it was created.
private readonly cachedPages = new Map<number, CachedPage>();
// Track in-flight RSC fetches keyed by the resolved request URL so that
// navigations can adopt an existing stream instead of issuing a duplicate
// request.
private readonly inflight = new Map<
string,
{ controller: AbortController; css: string[]; model: Promise<NonNullishReactNode> }
>();
public readonly css: BakeCSSManager = new BakeCSSManager();
public hasNavigatedSinceDOMContentLoaded(): boolean {
return this.lastNavigationId !== 0;
}
public setCachedPage(id: number, page: CachedPage): void {
this.cachedPages.set(id, page);
}
/** Start fetching an RSC payload for a given href without committing UI. */
public async prefetch(href: string): Promise<void> {
const requestUrl = this.computeRequestUrl(href);
if (this.inflight.has(requestUrl)) return;
const controller = new AbortController();
const signal = controller.signal;
let response: Response;
try {
response = await fetch(requestUrl, {
headers: { Accept: "text/x-component" },
signal,
});
if (!response.ok) return;
} catch {
return;
}
// Parse CSS list without mutating the active CSS set, and keep the stream
// intact for React consumption.
const { stream, list } = await this.css.readCssMetadataForPrefetch(response.body!);
const model = createFromReadableStream(stream) as Promise<NonNullishReactNode>;
this.inflight.set(requestUrl, { controller, css: list, model });
// Cleanup when the model settles to avoid leaks (if we never navigate).
void model.finally(() => {
// Do not delete if it's currently adopted by a navigation (i.e. lastNavigationController === controller)
if (this.inflight.get(requestUrl)?.controller === controller) return;
this.inflight.delete(requestUrl);
});
}
private computeRequestUrl(href: string): string {
const url = new URL(href, location.href);
url.hash = "";
if (import.meta.env.STATIC) {
// For static, fetch the .rsc artifact
const path = url.pathname.replace(/\/(?:index)?$/, "") + "/index.rsc";
return new URL(path + url.search, location.origin).toString();
}
return url.toString();
}
async navigate(href: string, cacheId: number | undefined): Promise<void> {
const thisNavigationId = ++this.lastNavigationId;
const olderController = this.lastNavigationController;
// If there is an in-flight prefetch for this href, adopt it.
const requestUrl = this.computeRequestUrl(href);
const adopted = this.inflight.get(requestUrl);
this.lastNavigationController = adopted?.controller ?? new AbortController();
const signal = this.lastNavigationController.signal;
signal.addEventListener(
"abort",
() => {
olderController?.abort();
},
{ once: true },
);
// If the page is cached, use the cached promise instead of fetching it again.
const cached = cacheId !== undefined && this.cachedPages.get(cacheId);
if (cached) {
await this.css.set(cached.css);
const state: AppState = {
rsc: cached.element,
};
if (olderController?.signal.aborted === false) {
state.abortOnRender = olderController;
}
setAppState(state);
return;
}
let p: NonNullishReactNode;
if (adopted) {
// Adopt prefetch: set CSS list and await the same model.
await this.css.set(adopted.css);
const cssWaitPromise = this.css.ensureCssIsReady();
{
const result = await adopted.model;
if (result == null) {
throw new Error("RSC payload was empty");
}
p = result as NonNullishReactNode;
}
if (thisNavigationId !== this.lastNavigationId) return;
if (cssWaitPromise) {
await cssWaitPromise;
if (thisNavigationId !== this.lastNavigationId) return;
}
// Remove from inflight now that it's adopted
this.inflight.delete(requestUrl);
} else {
let response: Response;
try {
response = await fetch(requestUrl, {
headers: { Accept: "text/x-component" },
signal,
});
if (!response.ok) {
throw new Error(`Failed to fetch ${href}: ${response.status} ${response.statusText}`);
}
} catch (err) {
if (thisNavigationId === this.lastNavigationId) {
// Bail out to browser navigation if this fetch fails.
console.error(err);
location.href = href;
}
return;
}
if (thisNavigationId !== this.lastNavigationId) return;
let stream = response.body!;
stream = await this.css.readCssMetadata(stream);
if (thisNavigationId !== this.lastNavigationId) return;
const cssWaitPromise = this.css.ensureCssIsReady();
{
const model = createFromReadableStream(stream) as Promise<NonNullishReactNode | undefined | null>;
const result = await model;
if (result == null) {
throw new Error("RSC payload was empty");
}
p = result as NonNullishReactNode;
}
if (thisNavigationId !== this.lastNavigationId) return;
if (cssWaitPromise) {
await cssWaitPromise;
if (thisNavigationId !== this.lastNavigationId) return;
}
}
// Save this promise so that pressing the back button in the browser navigates
// to the same instance of the old page, instead of re-fetching it.
if (cacheId !== undefined) {
this.cachedPages.set(cacheId, {
css: [...this.css.getList()],
element: p,
});
}
// Defer aborting a previous request until VERY late. If a previous stream is
// aborted while rendering, it will cancel the render, resulting in a flash of
// a blank page.
if (olderController?.signal.aborted === false) {
getAppState().abortOnRender = olderController;
}
// Tell react about the new page promise
if (document.startViewTransition) {
document.startViewTransition(() => {
flushSync(() => {
if (thisNavigationId === this.lastNavigationId) {
setAppState(old => ({
rsc: p,
abortOnRender: olderController ?? old.abortOnRender,
}));
}
});
});
} else {
setAppState(old => ({
rsc: p,
abortOnRender: olderController ?? old.abortOnRender,
}));
}
}
}

View File

@@ -0,0 +1,39 @@
import { useSyncExternalStore, type SetStateAction } from "react";
export interface Store<T> {
read(): T;
write(value: SetStateAction<T>): void;
subscribe(callback: () => void): () => boolean;
}
function notify(set: Set<() => void>) {
for (const callback of set) callback();
}
export function store<T>(init: T): Store<T> {
let value = init;
const subscribers = new Set<() => void>();
return {
read() {
return value;
},
write(next) {
const current = this.read();
const resolved = next instanceof Function ? next(current) : next;
if (Object.is(current, resolved)) return;
value = resolved;
notify(subscribers);
},
subscribe(callback) {
subscribers.add(callback);
return () => subscribers.delete(callback);
},
};
}
export function useStore<T>(store: Store<T>): T {
return useSyncExternalStore(store.subscribe, store.read, store.read);
}

View File

@@ -0,0 +1,31 @@
"use client";
import { router } from "../client/constants.ts";
export interface LinkProps extends React.ComponentProps<"a"> {
/**
* The URL to navigate to
*/
href: string;
}
export function Link(props: LinkProps): React.JSX.Element {
return (
<a
{...props}
onMouseEnter={e => {
void router.prefetch(props.href).catch(() => {});
if (props.onMouseEnter) props.onMouseEnter(e);
}}
onClick={async e => {
if (props.onClick) {
await (props.onClick(e) as void | Promise<void>);
if (e.defaultPrevented) return;
}
e.preventDefault();
await router.navigate(props.href, undefined);
}}
/>
);
}

View File

@@ -1,18 +1,14 @@
// This file is loaded in the SSR graph, meaning the `react-server` condition is
// no longer set. This means we can import client components, using `react-dom`
// to perform Server-side rendering (creating HTML) out of the RSC payload.
import { ssrManifest } from "bun:bake/server";
import { ssrManifest } from "bun:app/server";
import { EventEmitter } from "node:events";
import type { Readable } from "node:stream";
import * as React from "react";
import type { RenderToPipeableStreamOptions } from "react-dom/server";
import { renderToPipeableStream } from "react-dom/server.node";
import { createFromNodeStream, type Manifest } from "react-server-dom-bun/client.node.unbundled.js";
import type { MiniAbortSignal } from "./server";
// Verify that React 19 is being used.
if (!React.use) {
throw new Error("Bun's React integration requires React 19");
}
import type { MiniAbortSignal } from "./server.tsx";
import { createFromNodeStream, type Manifest } from "./vendor/react-server-dom-bun/client.node.unbundled.js";
const createFromNodeStreamOptions: Manifest = {
moduleMap: ssrManifest,
@@ -34,27 +30,29 @@ const createFromNodeStreamOptions: Manifest = {
// - https://github.com/devongovett/rsc-html-stream
export function renderToHtml(
rscPayload: Readable,
bootstrapModules: readonly string[],
bootstrapModules: string[],
signal: MiniAbortSignal,
): ReadableStream {
// Bun supports a special type of readable stream type called "direct",
// which provides a raw handle to the controller. We can bypass all of
// the Web Streams API (slow) and use the controller directly.
let stream: RscInjectionStream | null = null;
let abort: () => void;
let abort: (reason?: any) => void;
return new ReadableStream({
type: "direct",
pull(controller) {
// `createFromNodeStream` turns the RSC payload into a React component.
const promise = createFromNodeStream(rscPayload, {
// React takes in a manifest mapping client-side assets
// to the imports needed for server-side rendering.
moduleMap: ssrManifest,
moduleLoading: { prefix: "/" },
});
const promise: Promise<React.ReactNode> = createFromNodeStream(rscPayload, createFromNodeStreamOptions);
// The root is this "Root" component that unwraps the streamed promise
// with `use`, and then returning the parsed React component for the UI.
const Root: any = () => React.use(promise);
const Root: React.JSXElementConstructor<{}> = () => React.use(promise);
// If the signal is already aborted, we should not proceed
if (signal.aborted) {
controller.close(signal.aborted);
return Promise.reject(signal.aborted);
}
// If the signal is already aborted, we should not proceed
if (signal.aborted) {
@@ -64,13 +62,20 @@ export function renderToHtml(
// `renderToPipeableStream` is what actually generates HTML.
// Here is where React is told what script tags to inject.
let pipe: (stream: any) => void;
let pipe: (stream: NodeJS.WritableStream) => void;
stream = new RscInjectionStream(rscPayload, controller);
({ pipe, abort } = renderToPipeableStream(<Root />, {
bootstrapModules,
onShellReady() {
// The shell (including <head>) has been fully rendered
stream?.onShellReady();
},
onError(error) {
if (!signal.aborted) {
// Abort the rendering and close the stream
signal.aborted = error;
signal.aborted = error as Error;
abort();
if (signal.abort) signal.abort();
if (stream) {
@@ -80,7 +85,6 @@ export function renderToHtml(
},
}));
stream = new RscInjectionStream(rscPayload, controller);
pipe(stream);
return stream.finished;
@@ -97,16 +101,22 @@ export function renderToHtml(
// Static builds can not stream suspense boundaries as they finish, but instead
// produce a single HTML blob. The approach is otherwise similar to `renderToHtml`.
export function renderToStaticHtml(rscPayload: Readable, bootstrapModules: readonly string[]): Promise<Blob> {
export function renderToStaticHtml(
rscPayload: Readable,
bootstrapModules: NonNullable<RenderToPipeableStreamOptions["bootstrapModules"]>,
): Promise<Blob> {
const stream = new StaticRscInjectionStream(rscPayload);
const promise = createFromNodeStream(rscPayload, createFromNodeStreamOptions);
const Root = () => React.use(promise);
const promise = createFromNodeStream<React.ReactNode>(rscPayload, createFromNodeStreamOptions);
const Root: React.JSXElementConstructor<{}> = () => React.use(promise);
const { pipe } = renderToPipeableStream(<Root />, {
bootstrapModules,
// Only begin flowing HTML once all of it is ready. This tells React
// to not emit the flight chunks, just the entire HTML.
onAllReady: () => pipe(stream),
});
return stream.result;
}
@@ -116,14 +126,14 @@ const continueScriptTag = "<script>__bun_f.push(";
const enum HtmlState {
/** HTML is flowing, it is not an okay time to inject RSC data. */
Flowing,
Flowing = 1,
/** It is safe to inject RSC data. */
Boundary,
}
const enum RscState {
/** No RSC data has been written yet */
Waiting,
Waiting = 1,
/** Some but not all RSC data has been written */
Paused,
/** All RSC data has been written */
@@ -142,11 +152,13 @@ class RscInjectionStream extends EventEmitter {
rscHasEnded = false;
/** Shared state for decoding RSC data into UTF-8 strings */
decoder = new TextDecoder("utf-8", { fatal: true });
/** Track if the shell (including head) has been fully rendered */
shellReady = false;
/** Resolved when all data is written */
finished: Promise<void>;
finalize: () => void;
reject: (err: any) => void;
reject: (err: unknown) => void;
constructor(rscPayload: Readable, controller: ReadableStreamDirectController) {
super();
@@ -154,7 +166,7 @@ class RscInjectionStream extends EventEmitter {
const { resolve, promise, reject } = Promise.withResolvers<void>();
this.finished = promise;
this.finalize = x => (controller.close(), resolve(x));
this.finalize = () => (controller.close(), resolve());
this.reject = reject;
rscPayload.on("data", this.writeRscData.bind(this));
@@ -170,8 +182,12 @@ class RscInjectionStream extends EventEmitter {
});
}
write(data: Uint8Array) {
if (import.meta.env.DEV && process.env.VERBOSE_SSR)
onShellReady() {
this.shellReady = true;
}
write(data: Uint8Array<ArrayBuffer>) {
if (import.meta.env.DEV && process.env.VERBOSE_SSR) {
console.write(
"write" +
Bun.inspect(
@@ -182,6 +198,8 @@ class RscInjectionStream extends EventEmitter {
) +
"\n",
);
}
if (endsWithClosingScript(data)) {
// The HTML is not done yet, but it's a suitible time to inject RSC data.
const { controller } = this;
@@ -256,12 +274,14 @@ class RscInjectionStream extends EventEmitter {
destroy(e) {}
end(e) {}
end() {
return this;
}
}
class StaticRscInjectionStream extends EventEmitter {
rscPayloadChunks: Uint8Array[] = [];
chunks: (Uint8Array | string)[] = [];
rscPayloadChunks: Uint8Array<ArrayBuffer>[] = [];
chunks: (Uint8Array<ArrayBuffer> | string)[] = [];
result: Promise<Blob>;
finalize: (blob: Blob) => void;
reject: (error: Error) => void;
@@ -276,7 +296,7 @@ class StaticRscInjectionStream extends EventEmitter {
rscPayload.on("data", chunk => this.rscPayloadChunks.push(chunk));
}
write(chunk) {
write(chunk: Uint8Array<ArrayBuffer>) {
this.chunks.push(chunk);
}
@@ -285,7 +305,7 @@ class StaticRscInjectionStream extends EventEmitter {
const lastChunk = this.chunks[this.chunks.length - 1];
// Release assertions for React's behavior. If these break there will be malformed HTML.
if (typeof lastChunk === "string") {
if (typeof lastChunk === "string" || !lastChunk) {
this.destroy(new Error("The last chunk was expected to be a Uint8Array"));
return;
}
@@ -305,7 +325,7 @@ class StaticRscInjectionStream extends EventEmitter {
// Ignore flush requests from React.
}
destroy(error) {
destroy(error: Error) {
this.reject(error);
}
}
@@ -336,7 +356,7 @@ function writeManyFlightScriptData(
decoder: TextDecoder,
controller: { write: (str: string) => void },
) {
if (chunks.length === 1) return writeSingleFlightScriptData(chunks[0], decoder, controller);
if (chunks.length === 1) return writeSingleFlightScriptData(chunks[0]!, decoder, controller);
let i = 0;
try {
@@ -355,6 +375,7 @@ function writeManyFlightScriptData(
controller.write('Uint8Array.from(atob("');
for (; i < chunks.length; i++) {
const chunk = chunks[i];
if (!chunk) continue;
const base64 = btoa(String.fromCodePoint(...chunk));
controller.write(base64.slice(1, -1));
}

View File

@@ -0,0 +1,24 @@
{
"compilerOptions": {
"lib": ["ESNext", "DOM"],
"moduleResolution": "NodeNext",
"module": "NodeNext",
"target": "ESNext",
"strict": true,
"noEmit": true,
"useUnknownInCatchVariables": true,
"noImplicitOverride": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"exactOptionalPropertyTypes": true,
"noImplicitReturns": true,
"noUncheckedIndexedAccess": true,
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"isolatedModules": true,
"isolatedDeclarations": true,
"declaration": true,
"jsx": "react-jsx"
}
}

View File

@@ -0,0 +1 @@
*

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) Meta Platforms, Inc. and affiliates.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,5 @@
# react-server-dom-bun
Experimental React Flight bindings for DOM using Bun.
**Use it at your own risk.**

View File

@@ -0,0 +1 @@
export function createFromReadableStream<T>(readable: ReadableStream<T>): Promise<T>;

View File

@@ -0,0 +1,7 @@
'use strict';
if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/react-server-dom-bun-client.browser.production.js');
} else {
module.exports = require('./cjs/react-server-dom-bun-client.browser.development.js');
}

View File

@@ -0,0 +1,3 @@
'use strict';
module.exports = require('./client.browser');

View File

@@ -0,0 +1,7 @@
'use strict';
if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/react-server-dom-bun-client.node.production.js');
} else {
module.exports = require('./cjs/react-server-dom-bun-client.node.development.js');
}

View File

@@ -0,0 +1,20 @@
import type { SSRManifest } from "bun:app/server";
import type { Readable } from "node:stream";
export interface Manifest {
moduleMap: SSRManifest;
moduleLoading?: ModuleLoading;
}
export interface ModuleLoading {
prefix: string;
crossOrigin?: string;
}
export interface Options {
encodeFormAction?: any;
findSourceMapURL?: any;
environmentName?: string;
}
export function createFromNodeStream<T = any>(readable: Readable, manifest?: Manifest): Promise<T>;

View File

@@ -0,0 +1,7 @@
'use strict';
if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/react-server-dom-bun-client.node.unbundled.production.js');
} else {
module.exports = require('./cjs/react-server-dom-bun-client.node.unbundled.development.js');
}

View File

@@ -0,0 +1,3 @@
{
"type": "module"
}

View File

@@ -0,0 +1,12 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
'use strict';
throw new Error('Use react-server-dom-bun/client instead.');

View File

@@ -0,0 +1,91 @@
{
"name": "react-server-dom-bun",
"description": "React Server Components bindings for DOM using Bun. This is intended to be integrated into meta-frameworks. It is not intended to be imported directly.",
"version": "0.0.0-364a46e8-20250924",
"keywords": [
"react"
],
"homepage": "https://react.dev/",
"bugs": "https://github.com/facebook/react/issues",
"license": "MIT",
"files": [
"LICENSE",
"README.md",
"index.js",
"plugin.js",
"client.js",
"client.browser.js",
"client.node.js",
"client.node.unbundled.js",
"server.js",
"server.browser.js",
"server.node.js",
"server.node.unbundled.js",
"static.js",
"static.browser.js",
"static.node.js",
"static.node.unbundled.js",
"node-register.js",
"cjs/",
"esm/"
],
"exports": {
".": "./index.js",
"./plugin": "./plugin.js",
"./client": {
"node": "./client.node.js",
"browser": "./client.browser.js",
"default": "./client.browser.js"
},
"./client.browser": "./client.browser.js",
"./client.node": "./client.node.js",
"./client.node.unbundled": "./client.node.unbundled.js",
"./server": {
"react-server": {
"deno": "./server.browser.js",
"node": {
"webpack": "./server.node.js",
"default": "./server.node.unbundled.js"
},
"browser": "./server.browser.js"
},
"default": "./server.js"
},
"./server.browser": "./server.browser.js",
"./server.node": "./server.node.js",
"./server.node.unbundled": "./server.node.unbundled.js",
"./static": {
"react-server": {
"deno": "./static.browser.js",
"node": {
"webpack": "./static.node.js",
"default": "./static.node.unbundled.js"
},
"browser": "./static.browser.js"
},
"default": "./static.js"
},
"./static.browser": "./static.browser.js",
"./static.node": "./static.node.js",
"./static.node.unbundled": "./static.node.unbundled.js",
"./node-loader": "./esm/react-server-dom-webpack-node-loader.production.js",
"./node-register": "./node-register.js",
"./package.json": "./package.json"
},
"main": "index.js",
"repository": {
"type": "git",
"url": "https://github.com/facebook/react.git",
"directory": "packages/react-server-dom-bun"
},
"engines": {
"node": ">=0.10.0"
},
"peerDependencies": {
"react": "19.2.0-canary-364a46e8-20250924",
"react-dom": "19.2.0-canary-364a46e8-20250924"
},
"dependencies": {
"neo-async": "^2.6.1"
}
}

View File

@@ -0,0 +1,3 @@
'use strict';
module.exports = require('./cjs/react-server-dom-bun-plugin.js');

View File

@@ -0,0 +1,17 @@
'use strict';
var s;
if (process.env.NODE_ENV === 'production') {
s = require('./cjs/react-server-dom-bun-server.browser.production.js');
} else {
s = require('./cjs/react-server-dom-bun-server.browser.development.js');
}
exports.renderToReadableStream = s.renderToReadableStream;
exports.decodeReply = s.decodeReply;
exports.decodeAction = s.decodeAction;
exports.decodeFormState = s.decodeFormState;
exports.registerServerReference = s.registerServerReference;
exports.registerClientReference = s.registerClientReference;
exports.createClientModuleProxy = s.createClientModuleProxy;
exports.createTemporaryReferenceSet = s.createTemporaryReferenceSet;

View File

@@ -0,0 +1,6 @@
'use strict';
throw new Error(
'The React Server Writer cannot be used outside a react-server environment. ' +
'You must configure Node.js using the `--conditions react-server` flag.'
);

View File

@@ -0,0 +1,20 @@
'use strict';
var s;
if (process.env.NODE_ENV === 'production') {
s = require('./cjs/react-server-dom-bun-server.node.production.js');
} else {
s = require('./cjs/react-server-dom-bun-server.node.development.js');
}
exports.renderToReadableStream = s.renderToReadableStream;
exports.renderToPipeableStream = s.renderToPipeableStream;
exports.decodeReply = s.decodeReply;
exports.decodeReplyFromBusboy = s.decodeReplyFromBusboy;
exports.decodeReplyFromAsyncIterable = s.decodeReplyFromAsyncIterable;
exports.decodeAction = s.decodeAction;
exports.decodeFormState = s.decodeFormState;
exports.registerServerReference = s.registerServerReference;
exports.registerClientReference = s.registerClientReference;
exports.createClientModuleProxy = s.createClientModuleProxy;
exports.createTemporaryReferenceSet = s.createTemporaryReferenceSet;

View File

@@ -0,0 +1,23 @@
import type { ServerManifest } from "bun:app/server";
import type { ReactElement } from "react";
export interface PipeableStream<T> {
/** Returns the input, which should match the Node.js writable interface */
pipe: <T extends NodeJS.WritableStream>(destination: T) => T;
abort: () => void;
}
export function renderToPipeableStream<T = any>(
model: ReactElement,
webpackMap: ServerManifest,
options?: RenderToPipeableStreamOptions,
): PipeableStream<T>;
export interface RenderToPipeableStreamOptions {
onError?: (error: Error) => void;
identifierPrefix?: string;
onPostpone?: () => void;
temporaryReferences?: any;
environmentName?: string;
filterStackFrame?: () => boolean;
}

View File

@@ -0,0 +1,20 @@
'use strict';
var s;
if (process.env.NODE_ENV === 'production') {
s = require('./cjs/react-server-dom-bun-server.node.unbundled.production.js');
} else {
s = require('./cjs/react-server-dom-bun-server.node.unbundled.development.js');
}
exports.renderToReadableStream = s.renderToReadableStream;
exports.renderToPipeableStream = s.renderToPipeableStream;
exports.decodeReply = s.decodeReply;
exports.decodeReplyFromBusboy = s.decodeReplyFromBusboy;
exports.decodeReplyFromAsyncIterable = s.decodeReplyFromAsyncIterable;
exports.decodeAction = s.decodeAction;
exports.decodeFormState = s.decodeFormState;
exports.registerServerReference = s.registerServerReference;
exports.registerClientReference = s.registerClientReference;
exports.createClientModuleProxy = s.createClientModuleProxy;
exports.createTemporaryReferenceSet = s.createTemporaryReferenceSet;

View File

@@ -0,0 +1,12 @@
'use strict';
var s;
if (process.env.NODE_ENV === 'production') {
s = require('./cjs/react-server-dom-bun-server.browser.production.js');
} else {
s = require('./cjs/react-server-dom-bun-server.browser.development.js');
}
if (s.unstable_prerender) {
exports.unstable_prerender = s.unstable_prerender;
}

View File

@@ -0,0 +1,6 @@
'use strict';
throw new Error(
'The React Server Writer cannot be used outside a react-server environment. ' +
'You must configure Node.js using the `--conditions react-server` flag.'
);

View File

@@ -0,0 +1,15 @@
'use strict';
var s;
if (process.env.NODE_ENV === 'production') {
s = require('./cjs/react-server-dom-bun-server.node.production.js');
} else {
s = require('./cjs/react-server-dom-bun-server.node.development.js');
}
if (s.unstable_prerender) {
exports.unstable_prerender = s.unstable_prerender;
}
if (s.unstable_prerenderToNodeStream) {
exports.unstable_prerenderToNodeStream = s.unstable_prerenderToNodeStream;
}

View File

@@ -0,0 +1,12 @@
'use strict';
var s;
if (process.env.NODE_ENV === 'production') {
s = require('./cjs/react-server-dom-bun-server.node.unbundled.production.js');
} else {
s = require('./cjs/react-server-dom-bun-server.node.unbundled.development.js');
}
if (s.unstable_prerenderToNodeStream) {
exports.unstable_prerenderToNodeStream = s.unstable_prerenderToNodeStream;
}

1550
packages/bun-types/app.d.ts vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,278 +0,0 @@
declare module "bun" {
export namespace __experimental {
/**
* Base interface for static site generation route parameters.
*
* Supports both single string values and arrays of strings for dynamic route segments.
* This is typically used for route parameters like `[slug]`, `[...rest]`, or `[id]`.
*
* @warning These APIs are experimental and might be moved/changed in future releases.
*
* @example
* ```tsx
* // Simple slug parameter
* type BlogParams = { slug: string };
*
* // Multiple parameters
* type ProductParams = {
* category: string;
* id: string;
* };
*
* // Catch-all routes with string arrays
* type DocsParams = {
* path: string[];
* };
* ```
*/
export interface SSGParamsLike {
[key: string]: string | string[];
}
/**
* Configuration object for a single static route to be generated.
*
* Each path object contains the parameters needed to render a specific
* instance of a dynamic route at build time.
*
* @warning These APIs are experimental and might be moved/changed in future releases.
*
* @template Params - The shape of route parameters for this path
*
* @example
* ```tsx
* // Single blog post path
* const blogPath: SSGPath<{ slug: string }> = {
* params: { slug: "my-first-post" }
* };
*
* // Product page with multiple params
* const productPath: SSGPath<{ category: string; id: string }> = {
* params: {
* category: "electronics",
* id: "laptop-123"
* }
* };
*
* // Documentation with catch-all route
* const docsPath: SSGPath<{ path: string[] }> = {
* params: { path: ["getting-started", "installation"] }
* };
* ```
*/
export interface SSGPath<Params extends SSGParamsLike = SSGParamsLike> {
params: Params;
}
/**
* Array of static paths to be generated at build time.
*
* This type represents the collection of all route configurations
* that should be pre-rendered for a dynamic route.
*
* @warning These APIs are experimental and might be moved/changed in future releases.
*
* @template Params - The shape of route parameters for these paths
*
* @example
* ```tsx
* // Array of blog post paths
* const blogPaths: SSGPaths<{ slug: string }> = [
* { params: { slug: "introduction-to-bun" } },
* { params: { slug: "performance-benchmarks" } },
* { params: { slug: "getting-started-guide" } }
* ];
*
* // Mixed parameter types
* const productPaths: SSGPaths<{ category: string; id: string }> = [
* { params: { category: "books", id: "javascript-guide" } },
* { params: { category: "electronics", id: "smartphone-x" } }
* ];
* ```
*/
export type SSGPaths<Params extends SSGParamsLike = SSGParamsLike> = SSGPath<Params>[];
/**
* Props interface for SSG page components.
*
* This interface defines the shape of props that will be passed to your
* static page components during the build process. The `params` object
* contains the route parameters extracted from the URL pattern.
*
* @warning These APIs are experimental and might be moved/changed in future releases.
*
* @template Params - The shape of route parameters for this page
*
* @example
* ```tsx
* // Blog post component props
* interface BlogPageProps extends SSGPageProps<{ slug: string }> {
* // params: { slug: string } is automatically included
* }
*
* // Product page component props
* interface ProductPageProps extends SSGPageProps<{
* category: string;
* id: string;
* }> {
* // params: { category: string; id: string } is automatically included
* }
*
* // Usage in component
* function BlogPost({ params }: BlogPageProps) {
* const { slug } = params; // TypeScript knows slug is a string
* return <h1>Blog post: {slug}</h1>;
* }
* ```
*/
export interface SSGPageProps<Params extends SSGParamsLike = SSGParamsLike> {
params: Params;
}
/**
* React component type for SSG pages that can be statically generated.
*
* This type represents a React component that receives SSG page props
* and can be rendered at build time. The component can be either a regular
* React component or an async React Server Component for advanced use cases
* like data fetching during static generation.
*
* @warning These APIs are experimental and might be moved/changed in future releases.
*
* @template Params - The shape of route parameters for this page component
*
* @example
* ```tsx
* // Regular synchronous SSG page component
* const BlogPost: SSGPage<{ slug: string }> = ({ params }) => {
* return (
* <article>
* <h1>Blog Post: {params.slug}</h1>
* <p>This content was generated at build time!</p>
* </article>
* );
* };
*
* // Async React Server Component for data fetching
* const AsyncBlogPost: SSGPage<{ slug: string }> = async ({ params }) => {
* // Fetch data during static generation
* const post = await fetchBlogPost(params.slug);
* const author = await fetchAuthor(post.authorId);
*
* return (
* <article>
* <h1>{post.title}</h1>
* <p>By {author.name}</p>
* <div dangerouslySetInnerHTML={{ __html: post.content }} />
* </article>
* );
* };
*
* // Product page with multiple params and async data fetching
* const ProductPage: SSGPage<{ category: string; id: string }> = async ({ params }) => {
* const [product, reviews] = await Promise.all([
* fetchProduct(params.category, params.id),
* fetchProductReviews(params.id)
* ]);
*
* return (
* <div>
* <h1>{product.name}</h1>
* <p>Category: {params.category}</p>
* <p>Price: ${product.price}</p>
* <div>
* <h2>Reviews ({reviews.length})</h2>
* {reviews.map(review => (
* <div key={review.id}>{review.comment}</div>
* ))}
* </div>
* </div>
* );
* };
* ```
*/
export type SSGPage<Params extends SSGParamsLike = SSGParamsLike> = import("react").ComponentType<
SSGPageProps<Params>
>;
/**
* getStaticPaths is Bun's implementation of SSG (Static Site Generation) path determination.
*
* This function is called at your app's build time to determine which
* dynamic routes should be pre-rendered as static pages. It returns an
* array of path parameters that will be used to generate static pages for
* dynamic routes (e.g., [slug].tsx, [category]/[id].tsx).
*
* The function can be either synchronous or asynchronous, allowing you to
* fetch data from APIs, databases, or file systems to determine which paths
* should be statically generated.
*
* @warning These APIs are experimental and might be moved/changed in future releases.
*
* @template Params - The shape of route parameters for the dynamic route
*
* @returns An object containing an array of paths to be statically generated
*
* @example
* ```tsx
* // In pages/blog/[slug].tsx ———————————————————╮
* export const getStaticPaths: GetStaticPaths<{ slug: string }> = async () => {
* // Fetch all blog posts from your CMS or API at build time
* const posts = await fetchBlogPosts();
*
* return {
* paths: posts.map((post) => ({
* params: { slug: post.slug }
* }))
* };
* };
*
* // In pages/products/[category]/[id].tsx
* export const getStaticPaths: GetStaticPaths<{
* category: string;
* id: string;
* }> = async () => {
* // Fetch products from database
* const products = await db.products.findMany({
* select: { id: true, category: { slug: true } }
* });
*
* return {
* paths: products.map(product => ({
* params: {
* category: product.category.slug,
* id: product.id
* }
* }))
* };
* };
*
* // In pages/docs/[...path].tsx (catch-all route)
* export const getStaticPaths: GetStaticPaths<{ path: string[] }> = async () => {
* // Read documentation structure from file system
* const docPaths = await getDocumentationPaths('./content/docs');
*
* return {
* paths: docPaths.map(docPath => ({
* params: { path: docPath.split('/') }
* }))
* };
* };
*
* // Synchronous example with static data
* export const getStaticPaths: GetStaticPaths<{ id: string }> = () => {
* const staticIds = ['1', '2', '3', '4', '5'];
*
* return {
* paths: staticIds.map(id => ({
* params: { id }
* }))
* };
* };
* ```
*/
export type GetStaticPaths<Params extends SSGParamsLike = SSGParamsLike> = () => MaybePromise<{
paths: SSGPaths<Params>;
}>;
}
}

View File

@@ -20,10 +20,10 @@
/// <reference path="./deprecated.d.ts" />
/// <reference path="./redis.d.ts" />
/// <reference path="./shell.d.ts" />
/// <reference path="./experimental.d.ts" />
/// <reference path="./serve.d.ts" />
/// <reference path="./sql.d.ts" />
/// <reference path="./security.d.ts" />
/// <reference path="./app.d.ts" />
/// <reference path="./bun.ns.d.ts" />

View File

@@ -9,9 +9,7 @@
"mime-db": "^1.52.0",
"react": "^0.0.0-experimental-380f5d67-20241113",
"react-dom": "^0.0.0-experimental-380f5d67-20241113",
"react-refresh": "^0.0.0-experimental-380f5d67-20241113",
"react-server-dom-bun": "^0.0.0-experimental-603e6108-20241029",
"react-server-dom-webpack": "^0.0.0-experimental-380f5d67-20241113"
"react-refresh": "^0.0.0-experimental-380f5d67-20241113"
},
"scripts": {
"run": "node hello.js",

View File

@@ -33,6 +33,20 @@ pub fn VisitExpr(
};
}
// const well_known_client_only_react_hooks = [_][]const u8{
// "useState",
// "useEffect",
// "useLayoutEffect",
// "useReducer",
// "useRef",
// "useCallback",
// "useMemo",
// "useImperativeHandle",
// "useInsertionEffect",
// "useTransition",
// "useDeferredValue",
// };
const visitors = struct {
pub fn e_new_target(_: *P, expr: Expr, _: ExprIn) Expr {
// this error is not necessary and it is causing breakages
@@ -1434,42 +1448,12 @@ pub fn VisitExpr(
if (!ReactRefresh.isHookName(original_name)) break :try_record_hook;
if (p.options.features.react_fast_refresh) {
p.handleReactRefreshHookCall(e_, original_name);
} else if (
// If we're here it means we're in server component.
// Error if the user is using the `useState` hook as it
// is disallowed in server components.
//
// We're also specifically checking that the target is
// `.e_import_identifier`.
//
// Why? Because we *don't* want to check for uses of
// `useState` _inside_ React, and we know React uses
// commonjs so it will never be `.e_import_identifier`.
check_for_usestate: {
if (e_.target.data == .e_import_identifier) break :check_for_usestate true;
// Also check for `React.useState(...)`
if (e_.target.data == .e_dot and e_.target.data.e_dot.target.data == .e_import_identifier) {
const id = e_.target.data.e_dot.target.data.e_import_identifier;
const name = p.symbols.items[id.ref.innerIndex()].original_name;
break :check_for_usestate bun.strings.eqlComptime(name, "React");
}
break :check_for_usestate false;
}) {
bun.assert(p.options.features.server_components.isServerSide());
if (!bun.strings.startsWith(p.source.path.pretty, "node_modules") and
bun.strings.eqlComptime(original_name, "useState"))
{
p.log.addError(
p.source,
expr.loc,
std.fmt.allocPrint(
p.allocator,
"\"useState\" is not available in a server component. If you need interactivity, consider converting part of this to a Client Component (by adding `\"use client\";` to the top of the file).",
.{},
) catch |err| bun.handleOom(err),
) catch |err| bun.handleOom(err);
}
}
// Note: we do not check for `use${Hook}` here because at
// this stage, we do not yet know if the file we're parsing
// is exclusively used inside a client component module
// graph.
}
// Implement constant folding for 'string'.charCodeAt(n)

View File

@@ -10,6 +10,9 @@ pub const FrameworkRouter = @import("./bake/FrameworkRouter.zig");
pub const api_name = "app";
/// Zig version of the TS definition 'Bake.Options' in 'bake.d.ts'
// External functions for module loading
extern fn BakeGetDefaultExportFromModule(global: *jsc.JSGlobalObject, key: jsc.JSValue) jsc.JSValue;
pub const UserOptions = struct {
/// This arena contains some miscellaneous allocations at startup
arena: std.heap.ArenaAllocator,
@@ -26,7 +29,7 @@ pub const UserOptions = struct {
}
/// Currently, this function must run at the top of the event loop.
pub fn fromJS(config: JSValue, global: *jsc.JSGlobalObject) !UserOptions {
pub fn fromJS(config: JSValue, global: *jsc.JSGlobalObject, resolver: *bun.resolver.Resolver) !UserOptions {
var arena = std.heap.ArenaAllocator.init(bun.default_allocator);
errdefer arena.deinit();
const alloc = arena.allocator();
@@ -36,34 +39,6 @@ pub const UserOptions = struct {
var bundler_options = SplitBundlerOptions.empty;
if (!config.isObject()) {
// Allow users to do `export default { app: 'react' }` for convenience
if (config.isString()) {
const bunstr = try config.toBunString(global);
defer bunstr.deref();
const utf8_string = bunstr.toUTF8(bun.default_allocator);
defer utf8_string.deinit();
if (bun.strings.eql(utf8_string.byteSlice(), "react")) {
const root = bun.getcwdAlloc(alloc) catch |err| switch (err) {
error.OutOfMemory => {
return global.throwOutOfMemory();
},
else => {
return global.throwError(err, "while querying current working directory");
},
};
const framework = try Framework.react(alloc);
return UserOptions{
.arena = arena,
.allocations = allocations,
.root = root,
.framework = framework,
.bundler_options = bundler_options,
};
}
}
return global.throwInvalidArguments("'" ++ api_name ++ "' is not an object", .{});
}
@@ -79,10 +54,72 @@ pub const UserOptions = struct {
}
}
const framework = try Framework.fromJS(
try config.get(global, "framework") orelse {
return global.throwInvalidArguments("'" ++ api_name ++ "' is missing 'framework'", .{});
},
const framework_value = try config.get(global, "framework") orelse {
return global.throwInvalidArguments("'" ++ api_name ++ "' is missing 'framework'", .{});
};
const framework = if (framework_value.isString()) brk: {
const str = try framework_value.toBunString(global);
defer str.deref();
const name = allocations.track(str.toUTF8(alloc));
// Try to resolve "bun-framework-<name>" first
const prefixed_name = try std.fmt.allocPrint(alloc, "bun-framework-{s}", .{name});
const resolved_path = resolver.resolve(resolver.fs.top_level_dir, prefixed_name, .stmt) catch blk: {
// If that fails, try "<name>" directly
resolver.log.reset();
const direct_path = resolver.resolve(resolver.fs.top_level_dir, name, .stmt) catch {
return global.throwInvalidArguments("Failed to resolve framework package '{s}' (tried '{s}' and '{s}')", .{ name, prefixed_name, name });
};
break :blk direct_path;
};
const module_path_str = bun.String.cloneUTF8(resolved_path.pathConst().?.text);
defer module_path_str.deref();
var scope: jsc.CatchScope = undefined;
scope.init(global, @src());
defer scope.deinit();
const promise = jsc.JSModuleLoader.loadAndEvaluateModule(global, &module_path_str) orelse {
try scope.returnIfException();
return global.throwInvalidArguments("Failed to load framework module '{s}'", .{name});
};
const vm = global.vm();
promise.setHandled(vm);
global.bunVM().waitForPromise(.{ .internal = promise });
const result = switch (promise.unwrap(vm, .mark_handled)) {
.pending => unreachable,
.fulfilled => |resolved| blk: {
_ = resolved;
const default_export = BakeGetDefaultExportFromModule(global, module_path_str.toJS(global));
try scope.returnIfException();
if (!default_export.isObject()) {
return global.throwInvalidArguments("Framework module '{s}' must export an object as default", .{name});
}
break :blk try Framework.fromJS(
default_export,
global,
&allocations,
&bundler_options,
alloc,
);
},
.rejected => |err| {
_ = err;
try scope.returnIfException();
return global.throwInvalidArguments("Failed to load framework module '{s}'", .{name});
},
};
break :brk result;
} else try Framework.fromJS(
framework_value,
global,
&allocations,
&bundler_options,
@@ -244,56 +281,12 @@ const BuildConfigSubset = struct {
///
/// Full documentation on these fields is located in the TypeScript definitions.
pub const Framework = struct {
is_built_in_react: bool,
file_system_router_types: []FileSystemRouterType,
// static_routers: [][]const u8,
server_components: ?ServerComponents = null,
react_fast_refresh: ?ReactFastRefresh = null,
built_in_modules: bun.StringArrayHashMapUnmanaged(BuiltInModule) = .{},
/// Bun provides built-in support for using React as a framework.
/// Depends on externally provided React
///
/// $ bun i react@experimental react-dom@experimental react-refresh@experimental react-server-dom-bun
pub fn react(arena: std.mem.Allocator) !Framework {
return .{
.is_built_in_react = true,
.server_components = .{
.separate_ssr_graph = true,
.server_runtime_import = "react-server-dom-bun/server",
},
.react_fast_refresh = .{},
.file_system_router_types = try arena.dupe(FileSystemRouterType, &.{
.{
.root = "pages",
.prefix = "/",
.entry_client = "bun-framework-react/client.tsx",
.entry_server = "bun-framework-react/server.tsx",
.ignore_underscores = true,
.ignore_dirs = &.{ "node_modules", ".git" },
.extensions = &.{ ".tsx", ".jsx" },
.style = .nextjs_pages,
.allow_layouts = true,
},
}),
// .static_routers = try arena.dupe([]const u8, &.{"public"}),
.built_in_modules = bun.StringArrayHashMapUnmanaged(BuiltInModule).init(arena, &.{
"bun-framework-react/client.tsx",
"bun-framework-react/server.tsx",
"bun-framework-react/ssr.tsx",
}, if (Environment.codegen_embed) &.{
.{ .code = @embedFile("./bake/bun-framework-react/client.tsx") },
.{ .code = @embedFile("./bake/bun-framework-react/server.tsx") },
.{ .code = @embedFile("./bake/bun-framework-react/ssr.tsx") },
} else &.{
// Cannot use .import because resolution must happen from the user's POV
.{ .code = bun.runtimeEmbedFile(.src, "bake/bun-framework-react/client.tsx") },
.{ .code = bun.runtimeEmbedFile(.src, "bake/bun-framework-react/server.tsx") },
.{ .code = bun.runtimeEmbedFile(.src, "bake/bun-framework-react/ssr.tsx") },
}) catch |err| bun.handleOom(err),
};
}
/// Default that requires no packages or configuration.
/// - If `react-refresh` is installed, enable react fast refresh with it.
/// - Otherwise, if `react` is installed, use a bundled copy of
@@ -307,12 +300,7 @@ pub const Framework = struct {
file_system_router_types: []FileSystemRouterType,
) !Framework {
var fw: Framework = Framework.none;
if (file_system_router_types.len > 0) {
fw = try react(arena);
arena.free(fw.file_system_router_types);
fw.file_system_router_types = file_system_router_types;
}
fw.file_system_router_types = file_system_router_types;
if (resolveOrNull(resolver, "react-refresh/runtime")) |rfr| {
fw.react_fast_refresh = .{ .import_source = rfr };
@@ -333,7 +321,6 @@ pub const Framework = struct {
/// Unopiniated default.
pub const none: Framework = .{
.is_built_in_react = false,
.file_system_router_types = &.{},
.server_components = null,
.react_fast_refresh = null,
@@ -370,16 +357,6 @@ pub const Framework = struct {
import_source: []const u8 = "react-refresh/runtime",
};
pub const react_install_command = "bun i react@experimental react-dom@experimental react-server-dom-bun react-refresh@experimental";
pub fn addReactInstallCommandNote(log: *bun.logger.Log) !void {
try log.addMsg(.{
.kind = .note,
.data = try bun.logger.rangeData(null, bun.logger.Range.none, "Install the built in react integration with \"" ++ react_install_command ++ "\"")
.cloneLineText(log.clone_line_text, log.msgs.allocator),
});
}
/// Given a Framework configuration, this returns another one with all paths resolved.
/// New memory allocated into provided arena.
///
@@ -423,7 +400,17 @@ pub const Framework = struct {
had_errors.* = true;
return;
};
path.* = result.path().?.text;
const resolved_path = result.path() orelse {
bun.Output.err(error.ModuleNotFound, "Resolution returned null path for '{s}' ({s})", .{ path.*, desc });
had_errors.* = true;
return;
};
if (resolved_path.text.len == 0) {
bun.Output.err(error.ModuleNotFound, "Resolution returned empty path for '{s}' ({s})", .{ path.*, desc });
had_errors.* = true;
return;
}
path.* = resolved_path.text;
}
inline fn resolveOrNull(r: *bun.resolver.Resolver, path: []const u8) ?[]const u8 {
@@ -440,28 +427,16 @@ pub const Framework = struct {
bundler_options: *SplitBundlerOptions,
arena: Allocator,
) bun.JSError!Framework {
if (opts.isString()) {
const str = try opts.toBunString(global);
defer str.deref();
// Deprecated
if (str.eqlComptime("react-server-components")) {
bun.Output.warn("deprecation notice: 'react-server-components' will be renamed to 'react'", .{});
return Framework.react(arena);
}
if (str.eqlComptime("react")) {
return Framework.react(arena);
}
}
// This function should only be called with JS objects
// String handling happens in UserOptions.fromJS
if (!opts.isObject()) {
return global.throwInvalidArguments("Framework must be an object", .{});
return global.throwInvalidArguments("Framework definition must be an object", .{});
}
if (try opts.get(global, "serverEntryPoint") != null) {
bun.Output.warn("deprecation notice: 'framework.serverEntryPoint' has been replaced with 'fileSystemRouterTypes[n].serverEntryPoint'", .{});
}
if (try opts.get(global, "clientEntryPoint") != null) {
bun.Output.warn("deprecation notice: 'framework.clientEntryPoint' has been replaced with 'fileSystemRouterTypes[n].clientEntryPoint'", .{});
}
@@ -488,6 +463,7 @@ pub const Framework = struct {
.import_source = refs.track(str.toUTF8(arena)),
};
};
const server_components: ?ServerComponents = sc: {
const sc: JSValue = try opts.get(global, "serverComponents") orelse
break :sc null;
@@ -522,37 +498,7 @@ pub const Framework = struct {
"registerClientReference",
};
};
const built_in_modules: bun.StringArrayHashMapUnmanaged(BuiltInModule) = built_in_modules: {
const array = try opts.getArray(global, "builtInModules") orelse
break :built_in_modules .{};
const len = try array.getLength(global);
var files: bun.StringArrayHashMapUnmanaged(BuiltInModule) = .{};
try files.ensureTotalCapacity(arena, len);
var it = try array.arrayIterator(global);
var i: usize = 0;
while (try it.next()) |file| : (i += 1) {
if (!file.isObject()) {
return global.throwInvalidArguments("'builtInModules[{d}]' is not an object", .{i});
}
const path = try getOptionalString(file, global, "import", refs, arena) orelse {
return global.throwInvalidArguments("'builtInModules[{d}]' is missing 'import'", .{i});
};
const value: BuiltInModule = if (try getOptionalString(file, global, "path", refs, arena)) |str|
.{ .import = str }
else if (try getOptionalString(file, global, "code", refs, arena)) |str|
.{ .code = str }
else
return global.throwInvalidArguments("'builtInModules[{d}]' needs either 'path' or 'code'", .{i});
files.putAssumeCapacity(path, value);
}
break :built_in_modules files;
};
const file_system_router_types: []FileSystemRouterType = brk: {
const array: JSValue = try opts.getArray(global, "fileSystemRouterTypes") orelse {
return global.throwInvalidArguments("Missing 'framework.fileSystemRouterTypes'", .{});
@@ -646,11 +592,9 @@ pub const Framework = struct {
errdefer for (file_system_router_types) |*fsr| fsr.style.deinit();
const framework: Framework = .{
.is_built_in_react = false,
.file_system_router_types = file_system_router_types,
.react_fast_refresh = react_fast_refresh,
.server_components = server_components,
.built_in_modules = built_in_modules,
};
if (try opts.getOptional(global, "plugins", JSValue)) |plugin_array| {
@@ -934,13 +878,13 @@ pub fn addImportMetaDefines(
}
pub const server_virtual_source: bun.logger.Source = .{
.path = bun.fs.Path.initForKitBuiltIn("bun", "bake/server"),
.path = bun.fs.Path.initForKitBuiltIn("bun", "app/server"),
.contents = "", // Virtual
.index = bun.ast.Index.bake_server_data,
};
pub const client_virtual_source: bun.logger.Source = .{
.path = bun.fs.Path.initForKitBuiltIn("bun", "bake/client"),
.path = bun.fs.Path.initForKitBuiltIn("bun", "app/client"),
.contents = "", // Virtual
.index = bun.ast.Index.bake_client_data,
};

View File

@@ -20,19 +20,23 @@ bakeModuleLoaderImportModule(JSC::JSGlobalObject* global,
JSC::JSValue parameters,
const JSC::SourceOrigin& sourceOrigin)
{
WTF::String keyString = moduleNameValue->getString(global);
if (keyString.startsWith("bake:/"_s)) {
auto& vm = JSC::getVM(global);
auto& vm = JSC::getVM(global);
auto scope = DECLARE_THROW_SCOPE(vm);
auto keyString = moduleNameValue->value(global);
RETURN_IF_EXCEPTION(scope, nullptr);
if (keyString->isEmpty()) [[unlikely]] {
throwTypeError(global, scope, "import('') specifier cannot be empty"_s);
RETURN_IF_EXCEPTION(scope, nullptr);
}
if (keyString->startsWith("bake:/"_s)) {
return JSC::importModule(global, JSC::Identifier::fromString(vm, keyString),
JSC::jsUndefined(), parameters, JSC::jsUndefined());
}
if (!sourceOrigin.isNull() && sourceOrigin.string().startsWith("bake:/"_s)) {
auto& vm = JSC::getVM(global);
auto scope = DECLARE_THROW_SCOPE(vm);
WTF::String refererString = sourceOrigin.string();
WTF::String keyString = moduleNameValue->getString(global);
if (!keyString) {
auto promise = JSC::JSInternalPromise::create(vm, global->internalPromiseStructure());
@@ -136,7 +140,9 @@ JSC::JSInternalPromise* bakeModuleLoaderFetch(JSC::JSGlobalObject* globalObject,
if (global->m_perThreadData) [[likely]] {
BunString source = BakeProdLoad(global->m_perThreadData, Bun::toString(moduleKey));
if (source.tag != BunStringTag::Dead) {
JSC::SourceOrigin origin = JSC::SourceOrigin(WTF::URL(moduleKey));
WTF::URL url = WTF::URL(moduleKey);
ASSERT(url.isValid());
JSC::SourceOrigin origin = JSC::SourceOrigin(WTFMove(url));
JSC::SourceCode sourceCode = JSC::SourceCode(Bake::SourceProvider::create(
globalObject,
source.toWTFString(),

View File

@@ -414,8 +414,6 @@ pub fn init(options: Options) bun.JSOOM!*DevServer {
assert(dev.client_transpiler.resolver.opts.target == .browser);
dev.framework = dev.framework.resolve(&dev.server_transpiler.resolver, &dev.client_transpiler.resolver, options.arena) catch {
if (dev.framework.is_built_in_react)
try bake.Framework.addReactInstallCommandNote(&dev.log);
return global.throwValue(try dev.log.toJSAggregateError(global, bun.String.static("Framework is missing required files!")));
};

605
src/bake/bake.d.ts vendored
View File

@@ -1,605 +0,0 @@
// This API is under heavy development. See #bake in the Bun Discord for more info.
// Definitions that are commented out are planned but not implemented.
//
// To use, add a TypeScript reference comment mentioning this file:
// /// <reference path="/path/to/bun/src/bake/bake.d.ts" />
declare module "bun" {
declare namespace Bake {
interface Options {
/**
* Bun provides built-in support for using React as a framework by passing
* 'react' as the framework name. Otherwise, frameworks are config objects.
*
* External dependencies:
* ```
* bun i react@experimental react-dom@experimental react-server-dom-webpack@experimental react-refresh@experimental
* ```
*/
framework: Framework | "react";
// Note: To contribute to 'bun-framework-react', it can be run from this file:
// https://github.com/oven-sh/bun/blob/main/src/bake/bun-framework-react/index.ts
/**
* A subset of the options from Bun.build can be configured. While the framework
* can also set these options, this property overrides and merges with them.
*
* @default {}
*/
bundlerOptions?: BundlerOptions | undefined;
/**
* These plugins are applied after `framework.plugins`
*/
plugins?: BunPlugin[] | undefined;
}
/** Bake only allows a subset of options from `Bun.build` */
type BuildConfigSubset = Pick<
BuildConfig,
"conditions" | "define" | "loader" | "ignoreDCEAnnotations" | "drop"
// - format is not allowed because it is set to an internal "hmr" format
// - entrypoints/outfile/outdir doesnt make sense to set
// - disabling sourcemap is not allowed because it makes code impossible to debug
// - enabling minifyIdentifiers in dev is not allowed because some generated code does not support it
// - publicPath is set by the user (TODO: add options.publicPath)
// - emitDCEAnnotations is not useful
// - banner and footer do not make sense in these multi-file builds
// - disabling external would make it exclude imported files.
// - plugins is specified in the framework object, and currently merge between client and server.
// TODO: jsx customization
// TODO: chunk naming
>;
type BundlerOptions = BuildConfigSubset & {
/** Customize the build options of the client-side build */
client?: BuildConfigSubset;
/** Customize the build options of the server build */
server?: BuildConfigSubset;
/** Customize the build options of the separated SSR graph */
ssr?: BuildConfigSubset;
};
/**
* A "Framework" in our eyes is simply a set of bundler options that a
* framework author would set in order to integrate framework code with the
* application. Many of the configuration options are paths, which are
* resolved as import specifiers.
*/
interface Framework {
/**
* Customize the bundler options. Plugins in this array are merged
* with any plugins the user has.
* @default {}
*/
bundlerOptions?: BundlerOptions | undefined;
/**
* The translation of files to routes is unopinionated and left
* to framework authors. This interface allows most flexibility
* between the already established conventions while allowing
* new ideas to be explored too.
* @default []
*/
fileSystemRouterTypes?: FrameworkFileSystemRouterType[];
/**
* A list of directories that should be served statically. If the directory
* does not exist in the user's project, it is ignored.
*
* Example: 'public' or 'static'
*
* Different frameworks have different opinions, some use 'static', some
* use 'public'.
* @default []
*/
staticRouters?: Array<StaticRouter> | undefined;
/**
* Add extra modules. This can be used to, for example, replace `react`
* with a different resolution.
*
* Internally, Bun's `react-server-components` framework uses this to
* embed its files in the `bun` binary.
* @default {}
*/
builtInModules?: BuiltInModule[] | undefined;
/**
* Bun offers integration for React's Server Components with an
* interface that is generic enough to adapt to any framework.
* @default undefined
*/
serverComponents?: ServerComponentsOptions | undefined;
/**
* While it is unlikely that Fast Refresh is useful outside of
* React, it can be enabled regardless.
* @default false
*/
reactFastRefresh?: boolean | ReactFastRefreshOptions | undefined;
/** Framework bundler plugins load before the user-provided ones. */
plugins?: BunPlugin[];
// /**
// * Called after the list of routes is updated. This can be used to
// * implement framework-specific features like `.d.ts` generation:
// * https://nextjs.org/docs/app/building-your-application/configuring/typescript#statically-typed-links
// */
// onRouteListUpdate?: (routes: OnRouteListUpdateItem) => void;
}
/** Using `code` here will cause import resolution to happen from the root. */
type BuiltInModule = { import: string; code: string } | { import: string; path: string };
/**
* A high-level overview of what server components means exists
* in the React Docs: https://react.dev/reference/rsc/server-components
*
* When enabled, files with "use server" and "use client" directives will get
* special processing according to this object, in combination with the
* framework-specified entry points for server rendering and browser
* interactivity.
*/
interface ServerComponentsOptions {
/**
* If you are unsure what to set this to for a custom server components
* framework, choose 'false'.
*
* When set `true`, bundling "use client" components for SSR will be
* placed in a separate bundling graph without the `react-server`
* condition. All imports that stem from here get re-bundled for
* this second graph, regardless if they actually differ via this
* condition.
*
* The built in framework config for React enables this flag so that server
* components and client components utilize their own versions of React,
* despite running in the same process. This facilitates different aspects
* of the server and client react runtimes, such as `async` components only
* being available on the server.
*
* To cross from the server graph to the SSR graph, use the bun_bake_graph
* import attribute:
*
* import * as ReactDOM from 'react-dom/server' with { bunBakeGraph: 'ssr' };
*
* Since these models are so subtley different, there is no default value
* provided for this.
*/
separateSSRGraph: boolean;
/** Server components runtime for the server */
serverRuntimeImportSource: ImportSource;
/**
* When server code imports client code, a stub module is generated,
* where every export calls this export from `serverRuntimeImportSource`.
* This is used to implement client components on the server.
*
* When separateSSRGraph is enabled, the call looks like:
*
* export const ClientComp = registerClientReference(
* // A function which may be passed through, it throws an error
* function () { throw new Error('Cannot call client-component on the server') },
*
* // The file path. In production, these use hashed strings for
* // compactness and code privacy.
* "src/components/Client.tsx",
*
* // The instance id. This is not guaranteed to match the export
* // name the user has given.
* "ClientComp",
* );
*
* When separateSSRGraph is disabled, the call looks like:
*
* export const ClientComp = registerClientReference(
* function () { ... original user implementation here ... },
*
* // The file path of the client-side file to import in the browser.
* "/_bun/d41d8cd0.js",
*
* // The export within the client-side file to load. This is
* // not guaranteed to match the export name the user has given.
* "ClientComp",
* );
*
* While subtle, the parameters in `separateSSRGraph` mode are opaque
* strings that have to be looked up in the server manifest. While when
* there isn't a separate SSR graph, the two parameters are the actual
* URLs to load on the client; The manifest is not required for anything.
*
* Additionally, the bundler will assemble a component manifest to be used
* during rendering.
* @default "registerClientReference"
*/
serverRegisterClientReferenceExport?: string | undefined;
// /**
// * Allow creating client components inside of server-side files by using "use client"
// * as the first line of a function declaration. This is useful for small one-off
// * interactive components. This is behind a flag because it is not a feature of
// * React or Next.js, but rather is implemented because it is possible to.
// *
// * The client versions of these are tree-shaked extremely aggressively: anything
// * not referenced by the function body will be removed entirely.
// */
// allowAnonymousClientComponents: boolean;
}
/** Customize the React Fast Refresh transform. */
interface ReactFastRefreshOptions {
/**
* This import has four exports, mirroring "react-refresh/runtime":
*
* `injectIntoGlobalHook(window): void`
* Called on first startup, before the user entrypoint.
*
* `register(component, uniqueId: string): void`
* Called on every function that starts with an uppercase letter. These
* may or may not be components, but they are always functions.
*
* `createSignatureFunctionForTransform(): ReactRefreshSignatureFunction`
* TODO: document. A passing no-op for this api is `return () => {}`
*
* @default "react-refresh/runtime"
*/
importSource: ImportSource | undefined;
}
type ReactRefreshSignatureFunction = () =>
| void
| ((func: Function, hash: string, force?: bool, customHooks?: () => Function[]) => void);
/** This API is similar, but unrelated to `Bun.FileSystemRouter` */
interface FrameworkFileSystemRouterType {
/**
* Relative to project root. For example: `src/pages`.
*/
root: string;
/**
* The prefix to serve this directory on.
* @default "/"
*/
prefix?: string | undefined;
/**
* This file is the entrypoint of the server application. This module
* must `export default` a fetch function, which takes a request and the
* bundled route module, and returns a response. See `ServerEntryPoint`
*
* When `serverComponents` is configured, this can access the component
* manifest using the special 'bun:bake/server' import:
*
* import { serverManifest } from 'bun:bake/server'
*/
serverEntryPoint: ImportSource<ServerEntryPoint>;
/**
* This file is the true entrypoint of the client application. If null,
* a client will not be bundled, and the route will not receive bundling
* for client-side interactivity.
*/
clientEntryPoint?: ImportSource<ClientEntryPoint> | undefined;
/**
* Do not traverse into directories and files that start with an `_`. Do
* not index pages that start with an `_`. Does not prevent stuff like
* `_layout.tsx` from being recognized.
* @default false
*/
ignoreUnderscores?: boolean;
/**
* @default ["node_modules", ".git"]
*/
ignoreDirs?: string[];
/**
* Extensions to match on.
* '*' - any extension
* @default (set of all valid JavaScript/TypeScript extensions)
*/
extensions?: string[] | "*";
/**
* 'nextjs-app' builds routes out of directories with `page.tsx` and `layout.tsx`
* 'nextjs-pages' builds routes out of any `.tsx` file and layouts with `_layout.tsx`.
*
* Eventually, an API will be added to add custom styles.
*/
style: "nextjs-pages" | "nextjs-app-ui" | "nextjs-app-routes" | CustomFileSystemRouterFunction;
/**
* If true, this will track route layouts and provide them as an array during SSR.
* @default false
*/
layouts?: boolean | undefined;
// /**
// * If true, layouts act as navigation endpoints. This can be used to
// * implement Remix.run's router design, where `hello._index` and `hello`
// * are the same URL, but an allowed collision.
// *
// * @default false
// */
// navigatableLayouts?: boolean | undefined;
// /**
// * Controls how the route entry point is bundled with regards to server components:
// * - server-component: Default server components.
// * - client-boundary: As if "use client" was used on every route.
// * - disabled: As if server components was completely disabled.
// *
// * @default "server-component" if serverComponents is enabled, "disabled" otherwise
// */
// serverComponentsMode?: "server-component" | "client-boundary" | "disabled";
}
type StaticRouter =
/** Alias for { source: ..., prefix: "/" } */
| string
| {
/** The source directory to observe. */
source: string;
/** The prefix to serve this directory on. */
prefix: string;
};
/**
* Bun will call this function for every found file. This
* function classifies each file's role in the file system routing.
*/
type CustomFileSystemRouterFunction = (candidatePath: string) => CustomFileSystemRouterResult;
type CustomFileSystemRouterResult =
/** Skip this file */
| undefined
| null
/**
* Use this file as a route. Routes may nest, where a framework
* can use parent routes to implement layouts.
*/
| {
/**
* Route pattern can include `:param` for parameters, '*' for
* catch-all, and '*?' for optional catch-all. Parameters must take
* the full component of a path segment. Parameters cannot have
* constraints at this moment.
*/
pattern: string;
type: "route" | "layout" | "extra";
};
/**
* Will be resolved from the point of view of the framework user's project root
* Examples: `react-dom`, `./entry_point.tsx`, `/absolute/path.js`
*/
type ImportSource<T = unknown> = string;
interface ServerEntryPoint {
/**
* Bun passes the route's module as an opaque argument `routeModule`. The
* framework implementation decides and enforces the shape of the module.
*
* A common pattern would be to enforce the object is
* `{ default: ReactComponent }`
*/
render: (request: Request, routeMetadata: RouteMetadata) => MaybePromise<Response>;
/**
* Prerendering does not use a request, and is allowed to generate
* multiple responses. This is used for static site generation, but not
* not named `staticRender` as it is invoked during a dynamic build to
* allow deterministic routes to be prerendered.
*
* Note that `import.meta.env.STATIC` will be inlined to true during
* a static build.
*/
prerender?: (routeMetadata: RouteMetadata) => MaybePromise<PrerenderResult | null>;
// TODO: prerenderWithoutProps (for partial prerendering)
/**
* For prerendering routes with dynamic parameters, such as `/blog/:slug`,
* this will be called to get the list of parameters to prerender. This
* allows static builds to render every page at build time.
*
* `getParams` may return an object with an array of pages. For example,
* to generate two pages, `/blog/hello` and `/blog/world`:
*
* return {
* pages: [{ slug: 'hello' }, { slug: 'world' }],
* exhaustive: true,
* }
*
* "exhaustive" tells Bun that the list is complete. If it is not, a
* static site cannot be generated as it would otherwise be missing
* routes. A non-exhaustive list can speed up build times by only
* specifying a few important pages (such as 10 most recent), leaving
* the rest to be generated on-demand at runtime.
*
* To stream results, `getParams` may return an async iterator, which
* Bun will start rendering as more parameters are provided:
*
* export async function* getParams(meta: Bake.ParamsMetadata) {
* yield { slug: await fetchSlug() };
* yield { slug: await fetchSlug() };
* return { exhaustive: false };
* }
*/
getParams?: (paramsMetadata: ParamsMetadata) => MaybePromise<GetParamIterator>;
/**
* When a dynamic build uses static assets, Bun can map content types in the
* user's `Accept` header to the different static files.
*/
contentTypeToStaticFile?: Record<string, string>;
}
type GetParamIterator =
| AsyncIterable<Record<string, string | string[]>, GetParamsFinalOpts>
| Iterable<Record<string, string | string[]>, GetParamsFinalOpts>
| ({ pages: Array<Record<string, string | string[]>> } & GetParamsFinalOpts);
type GetParamsFinalOpts = void | null | {
/**
* @default true
*/
exhaustive?: boolean | undefined;
};
interface PrerenderResult {
files?: Record<string, Blob | NodeJS.TypedArray | ArrayBufferLike | string | Bun.BlobPart[]>;
// /**
// * For dynamic builds, `partialData` will be provided to `render` to allow
// * to implement Partial Pre-rendering, a technique where the a page shell
// * is rendered first, and the rendering is resumed. The bytes passed
// * here will be passed to the `render` function as `partialData`.
// */
// partialData?: Uint8Array;
// TODO: support incremental static regeneration + stale while revalidate here
// cache: unknown;
}
interface ClientEntryPoint {
// No exports
}
interface DevServerHookEntryPoint {
default: (dev: DevServerHookAPI) => MaybePromise<void>;
}
interface DevServerHookAPI {
// TODO:
}
/**
* This object and it's children may be re-used between invocations, so it
* is not safe to mutate it at all.
*/
interface RouteMetadata {
/**
* The loaded module of the page itself.
*/
readonly pageModule: any;
/**
* The loaded module of all of the route layouts. The first one is the
* inner-most, the last is the root layout.
*
* An example of converting the layout list into a nested JSX structure:
* const Page = meta.pageModule.default;
* let route = <Page />
* for (const layout of meta.layouts) {
* const Layout = layout.default;
* route = <Layout>{route}</Layout>;
* }
*/
readonly layouts: ReadonlyArray<any>;
/** Received route params. `null` if the route does not take params */
readonly params: null | Record<string, string | string[]>;
/**
* A list of js files that the route will need to be interactive.
*/
readonly modules: ReadonlyArray<string>;
/**
* A list of js files that should be preloaded.
*
* <link rel="modulepreload" href="..." />
*/
readonly modulepreload: ReadonlyArray<string>;
/**
* A list of css files that the route will need to be styled.
*/
readonly styles: ReadonlyArray<string>;
}
/**
* This object and it's children may be re-used between invocations, so it
* is not safe to mutate it at all.
*/
interface ParamsMetadata {
readonly pageModule: any;
readonly layouts: ReadonlyArray<any>;
}
}
declare interface BaseServeOptions {
/** Add a fullstack web app to this server using Bun Bake */
app?: Bake.Options | undefined;
}
declare interface PluginBuilder {
/**
* Inject a module into the development server's runtime, to be loaded
* before all other user code.
*/
addPreload(...args: any): void;
}
declare interface OnLoadArgs {
/**
* When using server-components, the same bundle has both client and server
* files; A single plugin can operate on files from both module graphs.
* Outside of server-components, this will be "client" when the target is
* set to "browser" and "server" otherwise.
*/
side: "server" | "client";
}
}
/** Available in server-side files only. */
declare module "bun:bake/server" {
// NOTE: The format of these manifests will likely be customizable in the future.
/**
* This follows the requirements for React's Server Components manifest, which
* is a mapping of component IDs to the client-side file it is exported in.
* The specifiers from here are to be imported in the client.
*
* To perform SSR with client components, see `ssrManifest`
*/
declare const serverManifest: ServerManifest;
/**
* Entries in this manifest map from client-side files to their respective SSR
* bundles. They can be loaded by `await import()` or `require()`.
*/
declare const ssrManifest: SSRManifest;
/** (insert teaser trailer) */
declare const actionManifest: never;
declare interface ServerManifest {
/**
* Concatenation of the component file ID and the instance id with '#'
* Example: 'components/Navbar.tsx#default' (dev) or 'l2#a' (prod/minified)
*
* The component file ID and the instance id are both passed to `registerClientReference`
*/
[combinedComponentId: string]: ServerManifestEntry;
}
declare interface ServerManifestEntry {
/**
* The `id` in ReactClientManifest.
* Correlates but is not required to be the filename
*/
id: string;
/**
* The `name` in ReactServerManifest
* Correlates but is not required to be the export name
*/
name: string;
/** Currently not implemented; always an empty array */
chunks: [];
}
declare interface SSRManifest {
/** ServerManifest[...].id */
[id: string]: {
/** ServerManifest[...].name */
[name: string]: SSRManifestEntry;
};
}
declare interface SSRManifestEntry {
/** Valid specifier to import */
specifier: string;
/** Export name */
name: string;
}
}
/** Available in client-side files. */
declare module "bun:bake/client" {
/**
* Callback is invoked when server-side code is changed. This can be used to
* fetch a non-html version of the updated page to perform a faster reload. If
* not provided, the client will perform a hard reload.
*
* Only one callback can be set. This function overwrites the previous one.
*/
export function onServerSideReload(cb: () => void | Promise<void>): Promise<void>;
}
/** Available during development */
declare module "bun:bake/dev" {}

View File

@@ -18,7 +18,7 @@ interface Config {
/** Dev Server's `configuration_hash_key` */
version: string;
/** If available, this is the Id of `react-refresh/runtime` */
refresh?: Id;
refresh?: Id | undefined;
/**
* A list of "roots" that the client is aware of. This includes
* the framework entry point, as well as every client component.
@@ -28,6 +28,7 @@ interface Config {
* If true, the client will receive console logs from the server.
*/
console: boolean;
generation: number;
}
/**
@@ -35,23 +36,23 @@ interface Config {
* Removed using --drop=ASSERT in releases.
*/
declare namespace DEBUG {
declare function ASSERT(condition: any, message?: string): asserts condition;
function ASSERT(condition: unknown, message?: string): asserts condition;
}
/** All modules for the initial bundle. */
declare const unloadedModuleRegistry: Record<string, UnloadedModule>;
declare const unloadedModuleRegistry: Record<string, UnloadedModule | true>;
declare type UnloadedModule = UnloadedESM | UnloadedCommonJS;
declare type UnloadedESM = [
deps: EncodedDependencyArray,
exportKeys: string[],
starImports: Id[],
load: (mod: import("./hmr-module").HMRModule) => Promise<void>,
load: (mod: import("./hmr-module.ts").HMRModule) => Promise<void>,
isAsync: boolean,
];
declare type EncodedDependencyArray = (string | number)[];
declare type UnloadedCommonJS = (
hmr: import("./hmr-module").HMRModule,
module: import("./hmr-module").HMRModule["cjs"],
hmr: import("./hmr-module.ts").HMRModule,
module: import("./hmr-module.ts").HMRModule["cjs"],
exports: unknown,
) => unknown;
declare type CommonJSModule = {
@@ -74,80 +75,20 @@ declare const side: "client" | "server";
* helpful information to someone working on the bundler itself. Assertions
* aimed for the end user should always be enabled.
*/
declare const IS_BUN_DEVELOPMENT: any;
declare var IS_BUN_DEVELOPMENT: unknown;
/** If this is the fallback error page */
declare const IS_ERROR_RUNTIME: boolean;
declare var __bun_f: any;
// The following interfaces have been transcribed manually.
declare module "react-server-dom-bun/client.browser" {
export function createFromReadableStream<T = any>(readable: ReadableStream<Uint8Array>): Promise<T>;
}
declare module "react-server-dom-bun/client.node.unbundled.js" {
import type { ReactClientManifest } from "bun:bake/server";
import type { Readable } from "node:stream";
export interface Manifest {
moduleMap: ReactClientManifest;
moduleLoading?: ModuleLoading;
}
export interface ModuleLoading {
prefix: string;
crossOrigin?: string;
}
export interface Options {
encodeFormAction?: any;
findSourceMapURL?: any;
environmentName?: string;
}
export function createFromNodeStream<T = any>(readable: Readable, manifest?: Manifest): Promise<T>;
}
declare module "react-server-dom-bun/server.node.unbundled.js" {
import type { ReactServerManifest } from "bun:bake/server";
import type { ReactElement } from "react";
export interface PipeableStream<T> {
/** Returns the input, which should match the Node.js writable interface */
pipe: <T>(destination: T) => T;
abort: () => void;
}
export function renderToPipeableStream<T = any>(
model: ReactElement,
webpackMap: ReactServerManifest,
options?: RenderToPipeableStreamOptions,
): PipeableStream<T>;
export interface RenderToPipeableStreamOptions {
onError?: Function;
identifierPrefix?: string;
onPostpone?: Function;
temporaryReferences?: any;
environmentName?: string;
filterStackFrame?: Function;
}
}
declare module "react-dom/server.node" {
import type { ReactElement } from "react";
import type { PipeableStream } from "react-server-dom-bun/server.node.unbundled.js";
export type RenderToPipeableStreamOptions = any;
export function renderToPipeableStream(
model: ReactElement,
options: RenderToPipeableStreamOptions,
): PipeableStream<Uint8Array>;
export * from "react-dom/server";
}
declare module "bun:wrap" {
export const __name: unique symbol;
export const __legacyDecorateClassTS: unique symbol;
export const __legacyDecorateParamTS: unique symbol;
export const __legacyMetadataTS: unique symbol;
export const __using: unique symbol;
export const __callDispose: unique symbol;
export const __name: unknown;
export const __legacyDecorateClassTS: unknown;
export const __legacyDecorateParamTS: unknown;
export const __legacyMetadataTS: unknown;
export const __using: unknown;
export const __callDispose: unknown;
}

View File

@@ -1,470 +0,0 @@
// This file contains the client-side logic for the built in React Server
// Components integration. It is designed as a minimal base to build RSC
// applications on, and to showcase what features that Bake offers.
/// <reference lib="dom" />
import { onServerSideReload } from "bun:bake/client";
import * as React from "react";
import { flushSync } from "react-dom";
import { hydrateRoot } from "react-dom/client";
import { createFromReadableStream } from "react-server-dom-bun/client.browser";
const te = new TextEncoder();
const td = new TextDecoder();
// It is the framework's responsibility to ensure that client-side navigation
// loads CSS files. The implementation here loads all CSS files as <link> tags,
// and uses the ".disabled" property to enable/disable them.
const cssFiles = new Map<string, { promise: Promise<void> | null; link: HTMLLinkElement }>();
let currentCssList: string[] | undefined = undefined;
// The initial RSC payload is put into inline <script> tags that follow the pattern
// `(self.__bun_f ??= []).push(chunk)`, which is converted into a ReadableStream
// here for React hydration. Since inline scripts are executed immediately, and
// this file is loaded asynchronously, the `__bun_f` becomes a clever way to
// stream the arbitrary data while HTML is loading. In a static build, this is
// setup as an array with one string.
let rscPayload: any = createFromReadableStream(
new ReadableStream({
start(controller) {
let handleChunk = chunk =>
typeof chunk === "string" //
? controller.enqueue(te.encode(chunk))
: controller.enqueue(chunk);
(self.__bun_f ||= []).forEach((__bun_f.push = handleChunk));
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => {
controller.close();
});
} else {
controller.close();
}
},
}),
);
// This is a function component that uses the `use` hook, which unwraps a
// promise. The promise results in a component containing suspense boundaries.
// This is the same logic that happens on the server, except there is also a
// hook to update the promise when the client navigates. The `Root` component
// also updates CSS files when navigating between routes.
let setPage;
let abortOnRender: AbortController | undefined;
const Root = () => {
setPage = React.useState(rscPayload)[1];
// Layout effects are executed right before the browser paints,
// which is the perfect time to make CSS visible.
React.useLayoutEffect(() => {
if (abortOnRender) {
try {
abortOnRender.abort();
abortOnRender = undefined;
} catch {}
}
requestAnimationFrame(() => {
if (currentCssList) disableUnusedCssFiles();
});
});
// Unwrap the promise if it is one
return rscPayload.then ? React.use(rscPayload) : rscPayload;
};
const root = hydrateRoot(document, <Root />, {
onUncaughtError(e) {
console.error(e);
},
});
// Keep a cache of page objects to avoid re-fetching a page when pressing the
// back button. The cache is indexed by the date it was created.
const cachedPages = new Map<number, Page>();
// const defaultPageExpiryTime = 1000 * 60 * 5; // 5 minutes
interface Page {
css: string[];
element: unknown;
}
const firstPageId = Date.now();
{
history.replaceState(firstPageId, "", location.href);
rscPayload.then(result => {
if (lastNavigationId > 0) return;
// Collect the list of CSS files that were added from SSR
const links = document.querySelectorAll<HTMLLinkElement>("link[data-bake-ssr]");
currentCssList = [];
for (let i = 0; i < links.length; i++) {
const link = links[i];
const href = new URL(link.href).pathname;
currentCssList.push(href);
// Hack: cannot add this to `cssFiles` because React owns the element, and
// it will be removed when any navigation is performed.
}
cachedPages.set(firstPageId, {
css: currentCssList!,
element: result,
});
});
if (document.startViewTransition as unknown) {
// View transitions are used by navigations to ensure that the page rerender
// all happens in one operation. Additionally, developers may animate
// different elements. The default fade animation is disabled so that the
// out-of-the-box experience feels like there are no view transitions.
// This is done client-side because a React error will unmount all elements.
const sheet = new CSSStyleSheet();
document.adoptedStyleSheets.push(sheet);
sheet.replaceSync(":where(*)::view-transition-group(root){animation:none}");
}
}
let lastNavigationId = 0;
let lastNavigationController: AbortController;
// Client side navigation is implemented by updating the app's `useState` with a
// new RSC payload promise. Callers of `goto` are expected to manage history state.
// A navigation id is used
async function goto(href: string, cacheId?: number) {
const thisNavigationId = ++lastNavigationId;
const olderController = lastNavigationController;
lastNavigationController = new AbortController();
const signal = lastNavigationController.signal;
signal.addEventListener("abort", () => {
olderController?.abort();
});
// If the page is cached, use the cached promise instead of fetching it again.
const cached = cacheId && cachedPages.get(cacheId);
if (cached) {
currentCssList = cached.css;
await ensureCssIsReady(currentCssList);
setPage?.((rscPayload = cached.element));
if (olderController?.signal.aborted === false) abortOnRender = olderController;
return;
}
let response: Response;
try {
// When using static builds, it isn't possible for the server to reliably
// branch on the `Accept` header. Instead, a static build creates a `.rsc`
// file that can be fetched. `import.meta.env.STATIC` is inlined by Bake.
response = await fetch(
import.meta.env.STATIC //
? `${href.replace(/\/(?:index)?$/, "")}/index.rsc`
: href,
{
headers: {
Accept: "text/x-component",
},
signal,
},
);
if (!response.ok) {
throw new Error(`Failed to fetch ${href}: ${response.status} ${response.statusText}`);
}
} catch (err) {
if (thisNavigationId === lastNavigationId) {
// Bail out to browser navigation if this fetch fails.
console.error(err);
location.href = href;
}
return;
}
// If the navigation id has changed, this fetch is no longer relevant.
if (thisNavigationId !== lastNavigationId) return;
let stream = response.body!;
// Read the css metadata at the start before handing it to react.
stream = await readCssMetadata(stream);
if (thisNavigationId !== lastNavigationId) return;
const cssWaitPromise = ensureCssIsReady(currentCssList!);
const p = await createFromReadableStream(stream);
if (thisNavigationId !== lastNavigationId) return;
if (cssWaitPromise) {
await cssWaitPromise;
if (thisNavigationId !== lastNavigationId) return;
}
// Save this promise so that pressing the back button in the browser navigates
// to the same instance of the old page, instead of re-fetching it.
if (cacheId) {
cachedPages.set(cacheId, { css: currentCssList!, element: p });
}
// Defer aborting a previous request until VERY late. If a previous stream is
// aborted while rendering, it will cancel the render, resulting in a flash of
// a blank page.
if (olderController?.signal.aborted === false) {
abortOnRender = olderController;
}
// Tell react about the new page promise
if (setPage) {
if (document.startViewTransition as unknown) {
document.startViewTransition(() => {
flushSync(() => {
if (thisNavigationId === lastNavigationId) setPage((rscPayload = p));
});
});
} else {
setPage((rscPayload = p));
}
}
}
// This function blocks until all CSS files are loaded.
function ensureCssIsReady(cssList: string[]) {
const wait: Promise<void>[] = [];
for (const href of cssList) {
const existing = cssFiles.get(href);
if (existing) {
const { promise, link } = existing;
if (promise) {
wait.push(promise);
}
link.disabled = false;
} else {
const link = document.createElement("link");
let entry;
const promise = new Promise<void>((resolve, reject) => {
link.rel = "stylesheet";
link.onload = resolve as any;
link.onerror = reject;
link.href = href;
document.head.appendChild(link);
}).then(() => {
entry.promise = null;
});
entry = { promise, link };
cssFiles.set(href, entry);
wait.push(promise);
}
}
if (wait.length === 0) return;
return Promise.all(wait);
}
function disableUnusedCssFiles() {
// TODO: create a list of files that should be updated instead of a full loop
for (const [href, { link }] of cssFiles) {
if (!currentCssList!.includes(href)) {
link.disabled = true;
}
}
}
// Instead of relying on a "<Link />" component, a global event listener on all
// clicks can be used. Care must be taken to intercept only anchor elements that
// did not have their default behavior prevented, non-left clicks, and more.
//
// This technique was inspired by SvelteKit which was inspired by https://github.com/visionmedia/page.js
document.addEventListener("click", async (event, element = event.target as HTMLAnchorElement) => {
if (
event.button ||
event.which != 1 ||
event.metaKey ||
event.ctrlKey ||
event.shiftKey ||
event.altKey ||
event.defaultPrevented
)
return;
while (element && element !== document.body) {
// This handles shadow roots
if (element.nodeType === 11) element = (element as any).host;
// If the current tag is an anchor.
if (element.nodeName.toUpperCase() === "A" && element.hasAttribute("href")) {
let url;
try {
url = new URL(element instanceof SVGAElement ? element.href.baseVal : element.href, document.baseURI);
} catch {
// Bail out to browser logic
return;
}
let pathname = url.pathname;
if (pathname.endsWith("/")) {
pathname = pathname.slice(0, -1);
}
// Ignore if the link is external
if (url.origin !== origin || (element.getAttribute("rel") || "").split(/\s+/).includes("external")) {
return;
}
// TODO: consider `target` attribute
// Take no action at all if the url is the same page.
// However if there is a hash, don't call preventDefault()
if (pathname === location.pathname && url.search === location.search) {
return url.hash || event.preventDefault();
}
const href = url.href;
const newId = Date.now();
history.pushState(newId, "", href);
goto(href, newId);
return event.preventDefault();
}
// Walk up the tree until an anchor or the body is found.
element = (element.assignedSlot ?? element.parentNode) as HTMLAnchorElement;
}
});
// Handle browser navigation events
window.addEventListener("popstate", event => {
let state = event.state;
if (typeof state !== "number") {
state = undefined;
}
goto(location.href, state);
});
if (import.meta.env.DEV) {
// Frameworks can call `onServerSideReload` to hook into server-side hot
// module reloading.
onServerSideReload(async () => {
const newId = Date.now();
history.replaceState(newId, "", location.href);
await goto(location.href, newId);
});
// Expose a global in Development mode
(window as any).$bake = {
goto,
onServerSideReload,
get currentCssList() {
return currentCssList;
},
};
}
async function readCssMetadata(stream: ReadableStream<Uint8Array>) {
let reader;
try {
// Using BYOB reader allows reading an exact amount of bytes, which allows
// passing the stream to react without creating a wrapped stream.
reader = stream.getReader({ mode: "byob" });
} catch (e) {
return readCssMetadataFallback(stream);
}
const header = (await reader.read(new Uint32Array(1))).value;
if (!header) {
if (import.meta.env.DEV) {
throw new Error("Did not read all bytes! This is a bug in bun-framework-react");
} else {
location.reload();
}
}
if (header[0] > 0) {
const cssRaw = (await reader.read(new Uint8Array(header[0]))).value;
if (!cssRaw) {
if (import.meta.env.DEV) {
throw new Error("Did not read all bytes! This is a bug in bun-framework-react");
} else {
location.reload();
}
}
currentCssList = td.decode(cssRaw).split("\n");
} else {
currentCssList = [];
}
reader.releaseLock();
return stream;
}
// Safari does not support BYOB reader. When this is resolved, this fallback
// should be kept for a few years since Safari on iOS is versioned to the OS.
// https://bugs.webkit.org/show_bug.cgi?id=283065
async function readCssMetadataFallback(stream: ReadableStream<Uint8Array>) {
const reader = stream.getReader();
const chunks: Uint8Array[] = [];
let totalBytes = 0;
const readChunk = async size => {
while (totalBytes < size) {
const { value, done } = await reader.read();
if (!done) {
chunks.push(value);
totalBytes += value.byteLength;
} else if (totalBytes < size) {
if (import.meta.env.DEV) {
throw new Error("Not enough bytes, expected " + size + " but got " + totalBytes);
} else {
location.reload();
}
}
}
if (chunks.length === 1) {
const first = chunks[0];
if (first.byteLength >= size) {
chunks[0] = first.subarray(size);
totalBytes -= size;
return first.subarray(0, size);
} else {
chunks.length = 0;
totalBytes = 0;
return first;
}
} else {
const buffer = new Uint8Array(size);
let i = 0;
let chunk;
let len;
while (size > 0) {
chunk = chunks.shift();
const { byteLength } = chunk;
len = Math.min(byteLength, size);
buffer.set(len === byteLength ? chunk : chunk.subarray(0, len), i);
i += len;
size -= len;
}
if (chunk.byteLength > len) {
chunks.unshift(chunk.subarray(len));
}
totalBytes -= size;
return buffer;
}
};
const header = new Uint32Array(await readChunk(4))[0];
if (header === 0) {
currentCssList = [];
} else {
currentCssList = td.decode(await readChunk(header)).split("\n");
}
if (chunks.length === 0) {
return stream;
}
// New readable stream that includes the remaining data
return new ReadableStream({
async start(controller) {
for (const chunk of chunks) {
controller.enqueue(chunk);
}
while (true) {
const { value, done } = await reader.read();
if (done) {
controller.close();
return;
}
controller.enqueue(value);
}
},
cancel() {
reader.cancel();
},
});
}

View File

@@ -1,42 +0,0 @@
// This file is unused by Bun itself, but rather is a tool for
// contributors to hack on `bun-framework-react` without needing
// to compile bun itself. If changes to this are made, please
// update 'pub fn react' in 'bake.zig'
import type { Bake } from "bun";
export function react(): Bake.Framework {
return {
// When the files are embedded in the Bun binary,
// relative path resolution does not work.
builtInModules: [
{ import: "bun-framework-react/client.tsx", path: require.resolve("./client.tsx") },
{ import: "bun-framework-react/server.tsx", path: require.resolve("./server.tsx") },
{ import: "bun-framework-react/ssr.tsx", path: require.resolve("./ssr.tsx") },
],
fileSystemRouterTypes: [
{
root: "pages",
clientEntryPoint: "bun-framework-react/client.tsx",
serverEntryPoint: "bun-framework-react/server.tsx",
extensions: ["jsx", "tsx"],
style: "nextjs-pages",
layouts: true,
ignoreUnderscores: true,
},
],
staticRouters: ["public"],
reactFastRefresh: {
importSource: "react-refresh/runtime",
},
serverComponents: {
separateSSRGraph: true,
serverRegisterClientReferenceExport: "registerClientReference",
serverRuntimeImportSource: "react-server-dom-webpack/server",
},
bundlerOptions: {
ssr: {
conditions: ["react-server"],
},
},
};
}

View File

@@ -1,10 +1,10 @@
import { td, te } from "../shared";
export class DataViewReader {
view: DataView;
view: DataView<ArrayBuffer>;
cursor: number;
constructor(view: DataView, cursor: number = 0) {
constructor(view: DataView<ArrayBuffer>, cursor: number = 0) {
this.view = view;
this.cursor = cursor;
}

View File

@@ -1,3 +1,15 @@
import { BundlerMessageLevel } from "../enums";
import { DataViewReader, DataViewWriter } from "./data-view";
import {
BundlerMessage,
BundlerMessageLocation,
BundlerNote,
decodeSerializedError,
type DeserializedFailure,
} from "./error-serialization";
import { syntaxHighlight } from "./JavaScriptSyntaxHighlighter";
import { parseStackTrace, type Frame } from "./stack-trace";
// This file implements the UI for error modals. Since using a framework like
// React could collide with the user's code (consider React DevTools), this
// entire modal is written from scratch using the standard DOM APIs. All CSS is
@@ -10,7 +22,6 @@
// Both use a WebSocket to coordinate followup updates, when new errors are
// added or previous ones are solved.
if (side !== "client") throw new Error("Not client side!");
// NOTE: imports are at the bottom for readability
/** When set, the next successful build will reload the page. */
export let hasFatalError = false;
@@ -55,40 +66,47 @@ let domNavBar: {
dismissAllBtn: HTMLButtonElement;
} = {} as any;
type TsLiteralStringables = string | number | bigint | boolean | null | undefined;
type PropsFor<T extends keyof HTMLElementTagNameMap> = null | Partial<{
[Key in keyof HTMLElementTagNameMap[T] as HTMLElementTagNameMap[T][Key] extends TsLiteralStringables
? Key
: never]: `${Extract<HTMLElementTagNameMap[T][Key], TsLiteralStringables>}`;
}>;
// I would have used JSX, but TypeScript types interfere in odd ways. However,
// this pattern allows concise construction of DOM nodes, but also extremely
// simple capturing of referenced nodes. Consider:
// let title;
// const btn = elem("button", { class: "file-name" }, [(title = textNode())]);
// Now you can edit `title.textContent` freely.
function elem<T extends keyof HTMLElementTagNameMap>(
tagName: T,
props?: null | Record<string, string>,
children?: Node[],
) {
function elem<T extends keyof HTMLElementTagNameMap>(tagName: T, props?: null | PropsFor<T>, children?: Node[]) {
const node = document.createElement(tagName);
if (props)
for (let key in props) {
node.setAttribute(key, props[key]);
}
if (children)
for (const child of children) {
node.appendChild(child);
if (props) {
for (const key in props) {
const value = props[key];
if (value === undefined) continue;
node.setAttribute(key, value);
}
}
if (children) {
for (const child of children) node.appendChild(child);
}
return node;
}
function elemText<T extends keyof HTMLElementTagNameMap>(
tagName: T,
props: null | Record<string, string>,
innerHTML: string,
) {
function elemText<T extends keyof HTMLElementTagNameMap>(tagName: T, props: null | PropsFor<T>, textContent: string) {
const node = document.createElement(tagName);
if (props)
for (let key in props) {
node.setAttribute(key, props[key]);
if (props) {
for (const key in props) {
const value = props[key];
if (value === undefined) continue;
node.setAttribute(key, value);
}
node.textContent = innerHTML;
}
node.textContent = textContent;
return node;
}
@@ -143,6 +161,7 @@ function mountModal() {
"background:#8883!important;" +
"z-index:2147483647!important",
});
const shadow = domShadowRoot.attachShadow({ mode: "open" });
const sheet = new CSSStyleSheet();
sheet.replace(OVERLAY_CSS);
@@ -469,7 +488,7 @@ function updateRuntimeErrorOverlay(err: RuntimeError) {
elem("div", { class: "message-desc error" }, [
elemText("code", { class: "name" }, name),
elemText("code", { class: "muted" }, ": "),
elemText("code", {}, err.message),
elemText("pre", {}, err.message.trim()),
]),
);
const { code } = err;
@@ -543,10 +562,11 @@ function updateBuildErrorOverlay({ remountAll = false }) {
// Create the element for the root if it does not yet exist.
if (!dom || remountAll) {
let fileName;
const fileName = textNode();
const root = elem("div", { class: "b-group" }, [
elem("div", { class: "trace-frame" }, [elem("div", { class: "file-name" }, [(fileName = textNode())])]),
elem("div", { class: "trace-frame" }, [elem("div", { class: "file-name" }, [fileName])]),
]);
dom = { root, fileName, messages: [] };
domErrorContent.appendChild(root);
errorDoms.set(owner, dom);
@@ -564,6 +584,7 @@ function updateBuildErrorOverlay({ remountAll = false }) {
dom.messages.push(domMessage);
}
}
updatedErrorOwners.clear();
}
@@ -669,15 +690,3 @@ declare global {
"bun-hmr": HTMLElement;
}
}
import { BundlerMessageLevel } from "../enums";
import { DataViewReader, DataViewWriter } from "./data-view";
import {
BundlerMessage,
BundlerMessageLocation,
BundlerNote,
decodeSerializedError,
type DeserializedFailure,
} from "./error-serialization";
import { syntaxHighlight } from "./JavaScriptSyntaxHighlighter";
import { parseStackTrace, type Frame } from "./stack-trace";

View File

@@ -52,7 +52,7 @@ function parseV8OrIE(stack: string): Frame[] {
return stack
.split("\n")
.filter(line => !!line.match(CHROME_IE_STACK_REGEXP) && !line.includes("Bun HMR Runtime"))
.map(function (line) {
.map((line): Frame => {
let sanitizedLine = line
.replace(/^\s+/, "")
.replace(/\(eval code/g, "(")
@@ -71,12 +71,14 @@ function parseV8OrIE(stack: string): Frame[] {
let functionName = (loc && sanitizedLine) || undefined;
let fileName = ["eval", "<anonymous>"].indexOf(locationParts[0]) > -1 ? undefined : locationParts[0];
return {
const frame: Frame = {
fn: functionName || "unknown",
file: fileName,
line: 0 | locationParts[1],
col: 0 | locationParts[2],
} satisfies Frame;
};
return frame;
});
}

View File

@@ -137,7 +137,7 @@ export function initWebSocket(
if (typeof data === "object") {
const view = new DataView(data);
if (IS_BUN_DEVELOPMENT) {
console.info("[WS] receive message '" + String.fromCharCode(view.getUint8(0)) + "',", new Uint8Array(data));
console.info("[WS] receive message '" + String.fromCharCode(view.getUint8(0)) + "'");
}
handlers[view.getUint8(0)]?.(view, ws);
}

View File

@@ -6,6 +6,9 @@
// Some build failures from the bundler surface as runtime errors here, such as
// `require` on a module with transitive top-level await, or a missing export.
// This was done to make incremental updates as isolated as possible.
// This import is different based on client vs server side.
import type { ServerManifest, SSRManifest } from "bun:app/server";
import {
__callDispose,
__legacyDecorateClassTS,
@@ -22,12 +25,12 @@ import { type SourceMapURL, derefMapping } from "#stack-trace";
const registry = new Map<Id, HMRModule>();
const registrySourceMapIds = new Map<string, SourceMapURL>();
/** Server */
export const serverManifest = {};
export const serverManifest: ServerManifest = {};
/** Server */
export const ssrManifest = {};
export const ssrManifest: SSRManifest = {};
/** Client */
export let onServerSideReload: (() => Promise<void>) | null = null;
const eventHandlers: Record<HMREvent | string, HotEventHandler[] | undefined> = {};
export let onServerSideReload: (() => Promise<void> | void) | null = null;
const eventHandlers: Record<Bun.HMREvent | string, HotEventHandler[] | undefined> = {};
let refreshRuntime: any;
/** The expression `import(a,b)` is not supported in all browsers, most notably
* in Mozilla Firefox in 2025. Bun lazily evaluates it, so a SyntaxError gets
@@ -170,19 +173,19 @@ export class HMRModule {
// not destructed.
accept(
arg1?: string | readonly string[] | HotAcceptFunction,
arg2?: HotAcceptFunction | HotArrayAcceptFunction | undefined,
specifiedOrAcceptFunctionOrFunctions?: string | readonly string[] | HotAcceptFunction,
acceptFunctionOrFunctions?: HotAcceptFunction | HotArrayAcceptFunction | undefined,
) {
if (arg2 == undefined) {
if (arg1 == undefined) {
if (acceptFunctionOrFunctions == undefined) {
if (specifiedOrAcceptFunctionOrFunctions == undefined) {
this.selfAccept = implicitAcceptFunction;
return;
}
if (typeof arg1 !== "function") {
if (typeof specifiedOrAcceptFunctionOrFunctions !== "function") {
throw new Error("import.meta.hot.accept requires a callback function");
}
// Self-accept function
this.selfAccept = arg1;
this.selfAccept = specifiedOrAcceptFunctionOrFunctions;
} else {
throw new Error(
'"import.meta.hot.accept" must be directly called with string literals for ' +
@@ -328,6 +331,11 @@ export function loadModuleSync(id: Id, isUserDynamic: boolean, importer: HMRModu
throw e;
}
}
if (loadOrEsmModule === true) {
throw new Error(
`Module "${id}" was resolved to a synthetic module, but did not have any exports. This is a bug in Bun.`,
);
}
const { [ESMProps.imports]: deps, [ESMProps.load]: load, [ESMProps.isAsync]: isAsync } = loadOrEsmModule;
if (isAsync) {
throw new AsyncImportError(id);
@@ -429,6 +437,13 @@ export function loadModuleAsync<IsUserDynamic extends boolean>(
throw e;
}
}
if (typeof loadOrEsmModule === "boolean") {
throw new Error(
`Module "${id}" was resolved to a synthetic module, but did not have any exports. This is a bug in Bun.`,
);
}
const [deps /* exports */ /* stars */, , , load /* isAsync */] = loadOrEsmModule;
if (!mod) {
@@ -588,19 +603,6 @@ type HotArrayAcceptFunction = (esmExports: (any | void)[]) => void;
type HotDisposeFunction = (data: any) => void | Promise<void>;
type HotEventHandler = (data: any) => void;
// If updating this, make sure the `devserver.d.ts` types are
// kept in sync.
type HMREvent =
| "bun:ready"
| "bun:beforeUpdate"
| "bun:afterUpdate"
| "bun:beforeFullReload"
| "bun:beforePrune"
| "bun:invalidate"
| "bun:error"
| "bun:ws:disconnect"
| "bun:ws:connect";
/** Called when modules are replaced. */
export async function replaceModules(modules: Record<Id, UnloadedModule>, sourceMapId?: SourceMapURL) {
Object.assign(unloadedModuleRegistry, modules);
@@ -815,15 +817,16 @@ function createAcceptArray(modules: string[], key: Id) {
return arr;
}
export function emitEvent(event: HMREvent, data: any) {
export function emitEvent(event: Bun.HMREvent, data: unknown) {
const handlers = eventHandlers[event];
if (!handlers) return;
for (const handler of handlers) {
handler(data);
}
}
export function onEvent(event: HMREvent, cb) {
export function onEvent(event: Bun.HMREvent, cb) {
(eventHandlers[event] ??= [])!.push(cb);
}
@@ -884,11 +887,22 @@ function toESM(mod: any) {
return to;
}
function registerSynthetic(id: Id, esmExports) {
const module = new HMRModule(id, false);
module.exports = esmExports;
registry.set(id, module);
unloadedModuleRegistry[id] = true as any;
/** Used to make sure our implementation is type-safe to what the type declarations say */
interface BakeBuiltinSyntheticModules {
"bun:app": typeof import("bun:app");
"bun:app/server": typeof import("bun:app/server");
"bun:app/client": typeof import("bun:app/client");
"bun:wrap": typeof import("bun:wrap");
}
function registerSynthetic<ModuleName extends keyof BakeBuiltinSyntheticModules>(
id: ModuleName,
esmExports: BakeBuiltinSyntheticModules[ModuleName],
) {
const mod = new HMRModule(id, false);
mod.exports = esmExports;
registry.set(id, mod);
unloadedModuleRegistry[id] = true;
}
export function setRefreshRuntime(runtime: HMRModule) {
@@ -906,26 +920,26 @@ export function setRefreshRuntime(runtime: HMRModule) {
// react-refresh/runtime does not provide this function for us
// https://github.com/facebook/metro/blob/febdba2383113c88296c61e28e4ef6a7f4939fda/packages/metro/src/lib/polyfills/require.js#L748-L774
function isReactRefreshBoundary(esmExports): boolean {
function isReactRefreshBoundary(moduleExports: unknown): boolean {
const { isLikelyComponentType } = refreshRuntime;
if (!isLikelyComponentType) return true;
if (isLikelyComponentType(esmExports)) {
if (isLikelyComponentType(moduleExports)) {
return true;
}
if (esmExports == null || typeof esmExports !== "object") {
if (moduleExports == null || typeof moduleExports !== "object") {
// Exit if we can't iterate over exports.
return false;
}
let hasExports = false;
let areAllExportsComponents = true;
for (const key in esmExports) {
for (const key in moduleExports) {
hasExports = true;
const desc = Object.getOwnPropertyDescriptor(esmExports, key);
const desc = Object.getOwnPropertyDescriptor(moduleExports, key);
if (desc && desc.get) {
// Don't invoke getters as they may have side effects.
return false;
}
const exportValue = esmExports[key];
const exportValue = moduleExports[key];
if (!isLikelyComponentType(exportValue)) {
areAllExportsComponents = false;
}
@@ -941,8 +955,6 @@ declare global {
}
}
// bun:bake/server, bun:bake/client, and bun:wrap are
// provided by this file instead of the bundler
registerSynthetic("bun:wrap", {
__name,
__legacyDecorateClassTS,
@@ -953,7 +965,7 @@ registerSynthetic("bun:wrap", {
});
if (side === "server") {
registerSynthetic("bun:bake/server", {
registerSynthetic("bun:app/server", {
serverManifest,
ssrManifest,
actionManifest: null,
@@ -961,7 +973,9 @@ if (side === "server") {
}
if (side === "client") {
registerSynthetic("bun:bake/client", {
onServerSideReload: cb => (onServerSideReload = cb),
registerSynthetic("bun:app/client", {
onServerSideReload: cb => {
onServerSideReload = cb;
},
});
}

View File

@@ -115,7 +115,7 @@ const handlers = {
ws.send("i" + config.generation);
}
},
[MessageId.hot_update](view) {
[MessageId.hot_update](view: DataView<ArrayBuffer>) {
const reader = new DataViewReader(view, 1);
// The code genearting each list is annotated with equivalent "List n"
@@ -316,6 +316,7 @@ testingHook?.({
try {
const { refresh } = config;
if (refresh) {
const refreshRuntime = await loadModuleAsync(refresh, false, null);
setRefreshRuntime(refreshRuntime);

View File

@@ -1,29 +1,28 @@
/// <reference types="../../build/debug/codegen/ErrorCode.d.ts" />
// This file is the entrypoint to the hot-module-reloading runtime.
// On the server, communication is established with `server_exports`.
import type { Bake } from "bun";
import type { ServerEntryPoint } from "bun:app";
import "./debug";
import { loadExports, replaceModules, serverManifest, ssrManifest } from "./hmr-module";
// import { AsyncLocalStorage } from "node:async_hooks";
const { AsyncLocalStorage } = require("node:async_hooks");
const { AsyncLocalStorage } = require("node:async_hooks") as {
AsyncLocalStorage: typeof import("node:async_hooks").AsyncLocalStorage;
};
if (typeof IS_BUN_DEVELOPMENT !== "boolean") {
throw new Error("DCE is configured incorrectly");
}
export type RequestContext = {
responseOptions: ResponseInit;
streaming: boolean;
streamingStarted?: boolean;
renderAbort?: (path: string, params: Record<string, any> | null) => never;
};
export type RequestContext = import("bun:app").__internal.RequestContext;
// Create the AsyncLocalStorage instance for propagating response options
const responseOptionsALS = new AsyncLocalStorage();
const responseOptionsALS = new AsyncLocalStorage<RequestContext>();
let asyncLocalStorageWasSet = false;
interface Exports {
handleRequest: (
req: Request,
req: Bun.BunRequest,
routerTypeMain: Id,
routeModules: Id[],
clientEntryUrl: string,
@@ -50,6 +49,20 @@ interface Exports {
) => void;
}
function validateStreaming(streaming: unknown) {
if (streaming !== true && streaming !== false) {
throw new Error("Value of `export const streaming` must be a boolean");
}
return streaming;
}
function validateMode(mode: unknown) {
if (mode !== "ssr" && mode !== "static") {
throw new Error("Value of `export const mode` must be 'ssr' or 'st'");
}
return mode;
}
declare let server_exports: Exports;
server_exports = {
async handleRequest(
@@ -78,7 +91,7 @@ server_exports = {
});
}
const exports = await loadExports<Bake.ServerEntryPoint>(routerTypeMain);
const exports = await loadExports<ServerEntryPoint>(routerTypeMain);
const serverRenderer = exports.render;
@@ -91,11 +104,18 @@ server_exports = {
const [pageModule, ...layouts] = await Promise.all(routeModules.map(loadExports));
let requestWithCookies = req;
if (pageModule === null || typeof pageModule !== "object") {
throw new Error(`Did not find any exports in the page module. Got: ${Bun.inspect(pageModule)}`);
}
let storeValue: RequestContext = {
const streaming = "streaming" in pageModule ? validateStreaming(pageModule.streaming) : false;
const mode = "mode" in pageModule ? validateMode(pageModule.mode) : "static";
const requestWithCookies = req;
const storeValue: RequestContext = {
responseOptions: {},
streaming: pageModule.streaming ?? false,
streaming,
};
try {
@@ -112,7 +132,7 @@ server_exports = {
modulepreload: [],
params,
// Pass request in metadata when mode is 'ssr'
request: pageModule.mode === "ssr" ? requestWithCookies : undefined,
request: mode === "ssr" ? requestWithCookies : undefined,
},
responseOptionsALS,
);
@@ -163,25 +183,25 @@ server_exports = {
if (componentManifestAdd) {
for (const uid of componentManifestAdd) {
try {
const exports = await loadExports<{}>(uid);
const exports = await loadExports<{}>(uid);
const client = {};
for (const exportName of Object.keys(exports)) {
serverManifest[uid + "#" + exportName] = {
id: uid,
name: exportName,
chunks: [],
};
client[exportName] = {
specifier: "ssr:" + uid,
name: exportName,
};
}
ssrManifest[uid] = client;
} catch (err) {
console.log(err);
if (exports === null) {
throw new Error(`Failed to load exports for ${uid}`);
}
const client = {};
for (const exportName of Object.keys(exports)) {
serverManifest[uid + "#" + exportName] = {
id: uid,
name: exportName,
chunks: [],
};
client[exportName] = {
specifier: "ssr:" + uid,
name: exportName,
};
}
ssrManifest[uid] = client;
}
}

View File

@@ -214,7 +214,7 @@ pub fn buildWithVm(ctx: bun.cli.Command.Context, cwd: []const u8, vm: *VirtualMa
, .{});
};
break :config try bake.UserOptions.fromJS(app, vm.global);
break :config try bake.UserOptions.fromJS(app, vm.global, &vm.global.bunVM().transpiler.resolver);
},
.rejected => |err| {
return global.throwValue(err.toError() orelse err);
@@ -257,8 +257,6 @@ pub fn buildWithVm(ctx: bun.cli.Command.Context, cwd: []const u8, vm: *VirtualMa
bun.assert(server_transpiler.env == client_transpiler.env);
framework.* = framework.resolve(&server_transpiler.resolver, &client_transpiler.resolver, allocator) catch {
if (framework.is_built_in_react)
try bake.Framework.addReactInstallCommandNote(server_transpiler.log);
Output.errGeneric("Failed to resolve all imports required by the framework", .{});
Output.flush();
server_transpiler.log.print(Output.errorWriter()) catch {};
@@ -447,6 +445,7 @@ pub fn buildWithVm(ctx: bun.cli.Command.Context, cwd: []const u8, vm: *VirtualMa
const runtime_file_index = maybe_runtime_file_index orelse {
bun.Output.panic("Runtime file not found. This is an unexpected bug in Bun. Please file a bug report on GitHub.", .{});
};
const any_client_chunks = any_client_chunks: {
for (bundled_outputs) |file| {
if (file.side) |s| {
@@ -457,6 +456,7 @@ pub fn buildWithVm(ctx: bun.cli.Command.Context, cwd: []const u8, vm: *VirtualMa
}
break :any_client_chunks false;
};
if (any_client_chunks) {
const runtime_file: *const OutputFile = &bundled_outputs[runtime_file_index];
_ = runtime_file.writeToDisk(root_dir, ".") catch |err| {

View File

@@ -4,12 +4,11 @@
"lib": ["ESNext", "DOM", "DOM.Iterable", "DOM.AsyncIterable"],
"baseUrl": ".",
"paths": {
"bun-framework-react/*": ["./bun-framework-react/*"],
"bun-framework-react/*": ["../../packages/bun-framework-react/*"],
"bindgen": ["../codegen/bindgen-lib"]
},
"jsx": "react-jsx",
"types": ["react/experimental"]
"jsx": "react-jsx"
},
"include": ["**/*.ts", "**/*.tsx", "../runtime.js", "../runtime.bun.js"],
"include": ["**/*.ts", "**/*.tsx", "../runtime.js", "../runtime.bun.js", "bake.private.d.ts"],
"references": [{ "path": "../../packages/bun-types" }]
}

View File

@@ -617,7 +617,7 @@ pub fn fromJS(
const route = try AnyRoute.fromJS(global, path, value, init_ctx) orelse {
return global.throwInvalidArguments(
\\'routes' expects a Record<string, Response | HTMLBundle | {[method: string]: (req: BunRequest) => Response|Promise<Response>}>
\\'routes' expects a Record<string, Response | HTMLBundle | {[method: string]: (req: BunRequest) => Response | Promise<Response>}>
\\
\\To bundle frontend apps on-demand with Bun.serve(), import HTML files.
\\
@@ -637,10 +637,10 @@ pub fn fromJS(
\\ },
\\ "/path": {
\\ GET(req) {
\\ return Response.json({ message: "Hello World" });
\\ return Response.json({ message: "Hello Get" });
\\ },
\\ POST(req) {
\\ return Response.json({ message: "Hello World" });
\\ return Response.json({ message: "Hello Post" });
\\ },
\\ },
\\ },
@@ -827,7 +827,7 @@ pub fn fromJS(
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);
args.bake = try bun.bake.UserOptions.fromJS(bake_args_js, global, &global.bunVM().transpiler.resolver);
}
}

View File

@@ -845,6 +845,15 @@ pub const LinkerContext = struct {
// needs to be done for JavaScript files, not CSS files.
if (chunk.content == .javascript) {
const sources = c.parse_graph.input_files.items(.source);
// If this is an entry point chunk with no parts (common for SSR chunks),
// we still need to include the entry point path in the hash
if (chunk.entry_point.is_entry_point and chunk.content.javascript.parts_in_chunk_in_order.len == 0) {
const source = &sources[chunk.entry_point.source_index];
hasher.write(source.path.namespace);
hasher.write(source.path.pretty);
}
for (chunk.content.javascript.parts_in_chunk_in_order) |part_range| {
const source: *Logger.Source = &sources[part_range.source_index.get()];

View File

@@ -1163,7 +1163,7 @@ pub const BundleV2 = struct {
this.linker.graph.takeAstOwnership();
}
/// This generates the two asts for 'bun:bake/client' and 'bun:bake/server'. Both are generated
/// This generates the two asts for 'bun:app/client' and 'bun:app/server'. Both are generated
/// at the same time in one pass over the SCB list.
pub fn processServerComponentManifestFiles(this: *BundleV2) OOM!void {
// If a server components is not configured, do nothing

View File

@@ -525,7 +525,6 @@ pub fn generateChunksInParallel(
},
.bake_extra = brk: {
if (c.framework == null or is_dev_server) break :brk .{};
if (!c.framework.?.is_built_in_react) break :brk .{};
var extra: OutputFile.BakeExtra = .{};
extra.bake_is_runtime = chunk.files_with_parts_in_chunk.contains(Index.runtime.get());

View File

@@ -387,6 +387,8 @@ pub const Command = struct {
expose_gc: bool = false,
preserve_symlinks_main: bool = false,
console_depth: ?u16 = null,
/// `--app` runs bun.app.ts/bun.app.js for Bun Bake
app: bool = false,
cpu_prof: struct {
enabled: bool = false,
name: []const u8 = "",
@@ -551,6 +553,7 @@ pub const Command = struct {
}
var next_arg = ((args_iter.next()) orelse return .AutoCommand);
while (next_arg.len > 0 and next_arg[0] == '-' and !(next_arg.len > 1 and next_arg[1] == 'e')) {
next_arg = ((args_iter.next()) orelse return .AutoCommand);
}
@@ -852,6 +855,10 @@ pub const Command = struct {
const ctx = try Command.init(allocator, log, .RunCommand);
ctx.args.target = .bun;
if (ctx.runtime_options.app and ctx.positionals.len == 0) {
@"bun --app"(ctx);
}
if (ctx.filters.len > 0 or ctx.workspaces) {
FilterRun.runScriptsWithFilter(ctx) catch |err| {
Output.prettyErrorln("<r><red>error<r>: {s}", .{@errorName(err)});
@@ -902,6 +909,10 @@ pub const Command = struct {
return try @"bun --eval --print"(ctx);
}
if (ctx.runtime_options.app and ctx.positionals.len == 0) {
@"bun --app"(ctx);
}
const extension: []const u8 = if (ctx.args.entry_points.len > 0)
std.fs.path.extension(ctx.args.entry_points[0])
else
@@ -1427,6 +1438,21 @@ pub const Command = struct {
try bun_js.Run.boot(ctx, entry_point_buf[0 .. cwd.len + trigger.len], null);
}
fn @"bun --app"(ctx: Context) noreturn {
bun.bake.printWarning();
Output.flush();
// Set the entry point to ./bun.app and let the resolver find .ts or .js
// This matches the logic in production.zig
var positionals_buf: [1][]const u8 = .{"./bun.app"};
ctx.positionals = &positionals_buf;
if (RunCommand.exec(ctx, .{ .bin_dirs_only = false, .log_errors = true, .allow_fast_run_for_extensions = false }) catch false) {
Global.exit(0);
}
Global.exit(1);
}
fn @"bun ./bun.lockb"(ctx: Context) !void {
for (bun.argv) |arg| {
if (strings.eqlComptime(arg, "--hash")) {

View File

@@ -123,6 +123,7 @@ pub const auto_or_run_params = [_]ParamType{
clap.parseParam("-b, --bun Force a script or package to use Bun's runtime instead of Node.js (via symlinking node)") catch unreachable,
clap.parseParam("--shell <STR> Control the shell used for package.json scripts. Supports either 'bun' or 'system'") catch unreachable,
clap.parseParam("--workspaces Run a script in all workspace packages (from the \"workspaces\" field in package.json)") catch unreachable,
clap.parseParam("--app Run the bun.app.ts (for Bun Bake)") catch unreachable,
};
pub const auto_only_params = [_]ParamType{
@@ -1268,6 +1269,16 @@ pub fn parse(allocator: std.mem.Allocator, ctx: Command.Context, comptime cmd: C
}
}
if (cmd == .RunCommand or cmd == .AutoCommand) {
if (args.flag("--app")) {
if (!bun.FeatureFlags.bake()) {
Output.errGeneric("To use the experimental \"--app\" option, upgrade to the canary build of bun via \"bun upgrade --canary\"", .{});
Global.exit(1);
}
ctx.runtime_options.app = true;
}
}
opts.resolve = Api.ResolveMode.lazy;
if (jsx_factory != null or

View File

@@ -39,7 +39,7 @@ declare function $isPromiseRejected(promise: Promise<any>): boolean;
/** Asserts the input is a promise. Returns `true` if the promise is pending */
declare function $isPromisePending(promise: Promise<any>): boolean;
declare const IS_BUN_DEVELOPMENT: boolean;
declare var IS_BUN_DEVELOPMENT: boolean;
/** Place this directly above a function declaration (like a decorator) to make it a getter. */
declare const $getter: never;
@@ -766,7 +766,6 @@ declare function $ERR_ASYNC_CALLBACK(name): TypeError;
declare function $ERR_AMBIGUOUS_ARGUMENT(arg, message): TypeError;
declare function $ERR_INVALID_FD_TYPE(type): TypeError;
declare function $ERR_IP_BLOCKED(ip): Error;
declare function $ERR_IPC_DISCONNECTED(): Error;
declare function $ERR_SERVER_NOT_RUNNING(): Error;
declare function $ERR_IPC_CHANNEL_CLOSED(): Error;

View File

@@ -1,9 +1,7 @@
//! JS code for bake
/// <reference path="../../bake/bake.d.ts" />
import type { Bake } from "bun";
import type { GetParamIterator, RouteMetadata, ServerEntryPoint } from "bun:app";
type FrameworkPrerender = Bake.ServerEntryPoint["prerender"];
type FrameworkGetParams = Bake.ServerEntryPoint["getParams"];
type FrameworkPrerender = ServerEntryPoint["prerender"];
type FrameworkGetParams = ServerEntryPoint["getParams"];
type TypeAndFlags = number;
type FileIndex = number;
@@ -25,7 +23,7 @@ export async function renderRoutesForProdStatic(
sourceRouteFiles: string[],
paramInformation: Array<null | string[]>,
styles: string[][],
): Promise<void> {
): Promise<void[]> {
$debug({
outBase,
allServerFiles,
@@ -61,7 +59,7 @@ export async function renderRoutesForProdStatic(
layouts,
pageModule,
params,
} satisfies Bake.RouteMetadata);
} satisfies RouteMetadata);
if (results == null) {
throw new Error(`Route ${JSON.stringify(sourceRouteFiles[i])} cannot be pre-rendered to a static page.`);
}
@@ -105,7 +103,7 @@ export async function renderRoutesForProdStatic(
return doGenerateRoute(type, noClient, i, layouts, pageModule, params);
}
let modulesForFiles = [];
let modulesForFiles: any[] = [];
for (const fileList of files) {
$assert(fileList.length > 0);
if (fileList.length > 1) {
@@ -131,7 +129,7 @@ export async function renderRoutesForProdStatic(
if (paramInformation[i] != null) {
const getParam = getParams[type];
$assert(getParam != null && $isCallable(getParam));
const paramGetter: Bake.GetParamIterator = await getParam({
const paramGetter: GetParamIterator = await getParam({
pageModule,
layouts,
});

View File

@@ -19,7 +19,7 @@
"process": "^0.11.10",
"punycode": "^2.3.1",
"querystring-es3": "^1.0.0-0",
"react-refresh": "0.17.0",
"react-refresh": "0.18.0",
"readable-stream": "^4.5.2",
"stream-http": "^3.2.0",
"string_decoder": "^1.3.0",
@@ -251,7 +251,7 @@
"randomfill": ["randomfill@1.0.4", "", { "dependencies": { "randombytes": "^2.0.5", "safe-buffer": "^5.1.0" } }, "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw=="],
"react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="],
"react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="],
"readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="],

View File

@@ -12,7 +12,7 @@
"license": "ISC",
"dependencies": {
"assert": "^2.1.0",
"react-refresh": "0.17.0",
"react-refresh": "0.18.0",
"browserify-zlib": "^0.2.0",
"buffer": "^6.0.3",
"console-browserify": "^1.2.0",

View File

@@ -8,7 +8,7 @@
"bindgenv2": ["./codegen/bindgenv2/lib.ts"]
}
},
"include": ["**/*.ts", "**/*.tsx"],
"include": ["**/*.ts", "**/*.tsx", "bake/bake.private.d.ts"],
// separate projects have extra settings that only apply in those scopes
"exclude": ["js", "bake", "init", "create", "bun.js/bindings/libuv"]
}

View File

@@ -1,4 +1,3 @@
/// <reference path="../../src/bake/bake.d.ts" />
/* Dev server tests can be run with `bun test` or in interactive mode with `bun run test.ts "name filter"`
*
* Env vars:
@@ -9,17 +8,16 @@
* To write files to a stable location:
* export BUN_DEV_SERVER_TEST_TEMP="/Users/clo/scratch/dev"
*/
import { Bake, BunFile, Subprocess } from "bun";
import fs, { readFileSync, realpathSync } from "node:fs";
import path from "node:path";
import os from "node:os";
import { $, BunFile, Subprocess } from "bun";
import * as Bake from "bun:app";
import { expect, Matchers } from "bun:test";
import { bunEnv, isASAN, isCI, isWindows, mergeWindowEnvs, tempDirWithFiles } from "harness";
import assert from "node:assert";
import { Matchers } from "bun:test";
import { EventEmitter } from "node:events";
// @ts-ignore
import fs, { readFileSync, realpathSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import { dedent } from "../bundler/expectBundled.ts";
import { bunEnv, bunExe, isASAN, isCI, isWindows, mergeWindowEnvs, tempDirWithFiles } from "harness";
import { expect } from "bun:test";
import { exitCodeMapStrings } from "./exit-code-map.mjs";
const ASAN_TIMEOUT_MULTIPLIER = isASAN ? 3 : 1;
@@ -1402,8 +1400,10 @@ async function installReactWithCache(root: string) {
}
}
} else {
// Install fresh and populate cache
await Bun.$`${bunExe()} i --linker=hoisted react@experimental react-dom@experimental react-server-dom-bun react-refresh@experimental && ${bunExe()} install --linker=hoisted`
await Bun.$`
cd ${bunFrameworkReactProjectRoot} && bun pm pack --filename=bun-framework-react.tgz
cd ${root} && bun add bun-framework-react@${bunFrameworkReactProjectRoot}/bun-framework-react.tgz
`
.cwd(root)
.env({ ...bunEnv })
.throws(true);
@@ -1425,6 +1425,7 @@ async function installReactWithCache(root: string) {
// Global React cache management
let reactCachePromise: Promise<void> | null = null;
const bunFrameworkReactProjectRoot = path.join(import.meta.dir, "..", "..", "packages", "bun-framework-react");
/**
* Ensures the React cache is populated. This is a global operation that
@@ -1437,24 +1438,23 @@ export async function ensureReactCache(): Promise<void> {
const cacheValid = cacheFiles.every(file => fs.existsSync(path.join(reactCacheDir, file)));
if (!cacheValid) {
// Create a temporary directory for installation
const tempInstallDir = fs.mkdtempSync(path.join(tempDir, "react-install-"));
// Create a minimal package.json
fs.writeFileSync(
path.join(tempInstallDir, "package.json"),
JSON.stringify({
name: "react-cache-install",
version: "1.0.0",
private: true,
}),
);
try {
// Install React packages
await Bun.$`${bunExe()} i --linker=hoisted react@experimental react-dom@experimental react-server-dom-bun react-refresh@experimental && ${bunExe()} install --linker=hoisted`
await $`
cd ${bunFrameworkReactProjectRoot} && bun pm pack --filename=bun-framework-react.tgz
cd ${tempInstallDir} && bun add bun-framework-react@${bunFrameworkReactProjectRoot}/bun-framework-react.tgz
`
.cwd(tempInstallDir)
.env({ ...bunEnv })
.env(bunEnv)
.throws(true);
// Copy to cache
@@ -1514,16 +1514,15 @@ const counts: Record<string, number> = {};
console.log("Dev server testing directory:", tempDir);
async function writeAll(root: string, files: FileObject) {
const promises: Promise<any>[] = [];
const promises: Promise<number>[] = [];
for (const [file, contents] of Object.entries(files)) {
const filename = path.join(root, file);
fs.mkdirSync(path.dirname(filename), { recursive: true });
const formattedContents =
typeof contents === "string" ? dedent(contents).replaceAll("{{root}}", root.replaceAll("\\", "\\\\")) : contents;
// @ts-expect-error the type of Bun.write is too strict
promises.push(Bun.write(filename, formattedContents));
}
await Promise.all(promises);
return await Promise.all(promises);
}
class OutputLineStream extends EventEmitter {
@@ -1689,15 +1688,10 @@ export function indexHtmlScript(htmlFiles: string[]) {
const skipTargets = [process.platform, isCI ? "ci" : null].filter(Boolean);
function testImpl<T extends DevServerTest>(
description: string,
options: T,
NODE_ENV: "development" | "production",
caller: string,
): T {
function testImpl(description: string, options: DevServerTest, NODE_ENV: "development" | "production", caller: string) {
if (interactive) return options;
const jest = (Bun as any).jest(caller);
const jest = Bun.jest(caller);
const basename = path.basename(caller, ".test" + path.extname(caller));
const count = (counts[basename] = (counts[basename] ?? 0) + 1);
@@ -1742,9 +1736,10 @@ function testImpl<T extends DevServerTest>(
path.join(root, "bun.app.ts"),
dedent`
${options.pluginFile ? `import plugins from './pluginFile.ts';` : "let plugins = undefined;"}
${options.framework === "react" ? `import reactFramework from 'bun-framework-react';` : ""}
export default {
app: {
framework: ${JSON.stringify(options.framework)},
framework: ${options.framework === "react" ? "reactFramework" : JSON.stringify(options.framework)},
plugins,
},
};
@@ -1872,7 +1867,9 @@ function testImpl<T extends DevServerTest>(
}
using stream = new OutputLineStream("dev", devProcess.stdout, devProcess.stderr);
devProcess.exited.then(exitCode => (stream.exitCode = exitCode));
const port = parseInt((await stream.waitForLine(/localhost:(\d+)/))[1], 10);
const startupTimeout =
(options.timeoutMultiplier ?? 1) * (isWindows ? 5000 : 1000) * (Bun.version.includes("debug") ? 6 : 1);
const port = parseInt((await stream.waitForLine(/localhost:(\d+)/, startupTimeout))[1], 10);
const dev = new Dev(root, port, devProcess, stream, NODE_ENV, options);
if (dev.nodeEnv === "development") {
await dev.connectSocket();
@@ -2026,7 +2023,7 @@ process.on("exit", () => {
}
});
export function devTest<T extends DevServerTest>(description: string, options: T): T {
export function devTest(description: string, options: DevServerTest) {
// Capture the caller name as part of the test tempdir
const callerLocation = snapshotCallerLocation();
const caller = stackTraceFileName(callerLocation);
@@ -2049,7 +2046,7 @@ devTest.only = function (description: string, options: DevServerTest) {
return testImpl(description, { ...options, only: true }, "development", caller);
};
export function prodTest<T extends DevServerTest>(description: string, options: T): T {
export function prodTest(description: string, options: DevServerTest) {
const callerLocation = snapshotCallerLocation();
const caller = stackTraceFileName(callerLocation);
assert(

View File

@@ -13,7 +13,8 @@ const platformPath = (path: string) => (process.platform === "win32" ? path.repl
describe("production", () => {
test("works with sourcemaps - error thrown in React component", async () => {
const dir = await tempDirWithBakeDeps("bake-production-sourcemap", {
"src/index.tsx": `export default { app: { framework: "react" } };`,
"src/index.tsx": `import framework from 'bun-framework-react';
export default { app: { framework } };`,
"pages/index.tsx": `export default function IndexPage() {
throw new Error("oh no!");
return <div>Hello World</div>;
@@ -21,10 +22,6 @@ describe("production", () => {
"package.json": JSON.stringify({
"name": "test-app",
"version": "1.0.0",
"devDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0",
},
}),
});
@@ -45,10 +42,11 @@ describe("production", () => {
test("import.meta properties are inlined in production build", async () => {
const dir = await tempDirWithBakeDeps("bake-production-import-meta", {
"src/index.tsx": `export default {
app: {
framework: "react",
}
"src/index.tsx": `import framework from 'bun-framework-react';
export default {
app: {
framework,
}
};`,
"pages/index.tsx": `
export default function IndexPage() {
@@ -138,10 +136,11 @@ export default function TestPage() {
test("import.meta properties are inlined in catch-all routes during production build", async () => {
const dir = await tempDirWithBakeDeps("bake-production-catch-all", {
"src/index.tsx": `export default {
app: {
framework: "react",
}
"src/index.tsx": `import framework from 'bun-framework-react';
export default {
app: {
framework,
}
};`,
"pages/blog/[...slug].tsx": `
export default function BlogPost({ params }) {
@@ -304,14 +303,11 @@ export default function GettingStarted() {
test("handles build with no pages directory without crashing", async () => {
const dir = await tempDirWithBakeDeps("bake-production-no-pages", {
"app.ts": `export default { app: { framework: "react" } };`,
"app.ts": `import framework from 'bun-framework-react';
export default { app: { framework } };`,
"package.json": JSON.stringify({
"name": "test-app",
"version": "1.0.0",
"devDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0",
},
}),
});
@@ -331,7 +327,8 @@ export default function GettingStarted() {
test("client-side component with default import should work", async () => {
const dir = await tempDirWithBakeDeps("bake-production-client-import", {
"src/index.tsx": `export default { app: { framework: "react" } };`,
"src/index.tsx": `import framework from 'bun-framework-react';
export default { app: { framework } };`,
"pages/index.tsx": `import Client from "../components/Client";
export default function IndexPage() {
@@ -351,10 +348,6 @@ export default function Client() {
"package.json": JSON.stringify({
"name": "test-app",
"version": "1.0.0",
"devDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0",
},
}),
});
@@ -374,9 +367,11 @@ export default function Client() {
expect(htmlContent).toContain("Hello World");
});
test("importing useState server-side", async () => {
// Skipped because we removed the check: src/ast/visitExpr.zig:1453
test.skip("importing useState server-side", async () => {
const dir = await tempDirWithBakeDeps("bake-production-react-import", {
"src/index.tsx": `export default { app: { framework: "react" } };`,
"src/index.tsx": `import framework from 'bun-framework-react';
export default { app: { framework } };`,
"pages/index.tsx": `import { useState } from 'react';
export default function IndexPage() {
@@ -392,10 +387,6 @@ export default function IndexPage() {
"package.json": JSON.stringify({
"name": "test-app",
"version": "1.0.0",
"devDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0",
},
}),
});
@@ -411,16 +402,17 @@ export default function IndexPage() {
test("importing useState from client component", async () => {
const dir = await tempDirWithBakeDeps("bake-production-client-useState", {
"src/index.tsx": `
const bundlerOptions = {
"src/index.tsx": `import framework from 'bun-framework-react';
const bundlerOptions = {
sourcemap: "inline",
minify: {
whitespace: false,
identifiers: false,
syntax: false,
},
};
export default { app: { framework: "react", bundlerOptions: { server: bundlerOptions, client: bundlerOptions, ssr: bundlerOptions } } };`,
};
export default { app: { framework, bundlerOptions: { server: bundlerOptions, client: bundlerOptions, ssr: bundlerOptions } } };`,
"pages/index.tsx": `import Counter from "../components/Counter";
export default function IndexPage() {
@@ -447,18 +439,16 @@ export default function Counter() {
"package.json": JSON.stringify({
"name": "test-app",
"version": "1.0.0",
"devDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0",
},
}),
});
console.log(dir);
// Run the build command
const { exitCode, stderr } = await Bun.$`${bunExe()} build --app ./src/index.tsx`.cwd(dir).throws(false);
const { exitCode, stdout, stderr } = await Bun.$`${bunExe()} build --app ./src/index.tsx`.cwd(dir).throws(false);
// The build should succeed - client components CAN use useState
expect(stderr.toString()).not.toContain("useState");
expect(stdout.toString(), stderr.toString()).not.toContain("useState");
expect(exitCode).toBe(0);
// Check the generated HTML file
@@ -502,7 +492,8 @@ export default function Counter() {
test("don't include client code if fully static route", async () => {
const dir = await tempDirWithBakeDeps("bake-production-no-client-js", {
"src/index.tsx": `export default { app: { framework: "react" } };`,
"src/index.tsx": `import framework from 'bun-framework-react';
export default { app: { framework } };`,
"pages/index.tsx": `
export default function IndexPage() {
return (
@@ -514,30 +505,19 @@ export default function IndexPage() {
"package.json": JSON.stringify({
"name": "test-app",
"version": "1.0.0",
"devDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0",
},
}),
});
// Run the build command
const { exitCode, stderr } = await Bun.$`${bunExe()} build --app ./src/index.tsx`.cwd(dir).throws(false);
// The build should succeed
// expect(stderr.toString()).toBe("");
expect(exitCode).toBe(0);
// Check the generated HTML file
const htmlPage = path.join(dir, "dist", "index.html");
expect(existsSync(htmlPage)).toBe(true);
const htmlContent = await Bun.file(htmlPage).text();
// Verify the content is rendered
expect(htmlContent).toContain("Hello World");
// Verify NO JavaScript imports are included in the HTML
expect(htmlContent).not.toContain('<script type="module"');
});
});

View File

@@ -26,24 +26,15 @@ export async function getStaticPaths() {
},
framework: "react",
async test(dev) {
// Make a request that will trigger the error
await dev.fetch("/test-error").catch(() => {});
// The output we saw shows the stack trace with correct source mapping
// We need to check that the error shows the right file:line:column
const lines = dev.output.lines.join("\n");
// Check that we got the error
expect(lines).toContain("Test error for source maps!");
// Check that the stack trace shows correct file and line numbers
// The source maps are working if we see the correct patterns
// We need to check for the patterns because ANSI codes might be embedded
// Strip ANSI codes for cleaner checking
const cleanLines = lines.replace(/\x1b\[[0-9;]*m/g, "");
const cleanLines = Bun.stripANSI(lines);
const hasCorrectThrowLine = cleanLines.includes("myFunc") && cleanLines.includes("6:16");
// const hasCorrectCallLine = cleanLines.includes("MyPage") && cleanLines.includes("2") && cleanLines.includes("3");
const hasCorrectThrowLine = cleanLines.includes("myFunc") && cleanLines.includes("6:1");
const hasCorrectFileName = cleanLines.includes("pages/[...slug].tsx");
expect(hasCorrectThrowLine).toBe(true);

View File

@@ -156,7 +156,7 @@ devTest("SSG pages router - hot reload on page changes", {
);
// this %c%s%c is a react devtools thing and I don't know how to turn it off
await c.expectMessage("%c%s%c updated load");
await c.expectMessage("[%s] updated load");
expect(await c.elemText("h1")).toBe("Updated Content");
},
});

View File

@@ -1,4 +1,3 @@
// @ts-nocheck
import { createRoot } from "react-dom/client";
import App from "./App";
createRoot(document.getElementById("root")!).render(<App />);

View File

@@ -1,6 +1,6 @@
{
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
"react": "19.2.0",
"react-dom": "19.2.0"
}
}

View File

@@ -1,4 +1,4 @@
import { Bake } from "bun";
import * as Bake from "bun:app";
export function render(req: Request, meta: Bake.RouteMetadata) {
if (typeof meta.pageModule.default !== "function") {

View File

@@ -119,7 +119,7 @@
},
},
"overrides": {
"react": "../node_modules/react",
"@types/node": "24.3.1",
},
"packages": {
"@adobe/css-tools": ["@adobe/css-tools@4.4.1", "", {}, "sha512-12WGKBQzjUAI4ayyF4IAtfw2QR/IDoqk6jTddXDhtYTJF9ASmoE1zst7cVtP0aL/F1jUJL5r+JxKXKEgHNbEUQ=="],
@@ -710,7 +710,7 @@
"@types/nlcst": ["@types/nlcst@2.0.3", "", { "dependencies": { "@types/unist": "*" } }, "sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA=="],
"@types/node": ["@types/node@20.14.6", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-JbA0XIJPL1IiNnU7PFxDXyfAwcwVVrOoqyzzyQTyMeVhBzkJVMSkC1LlVsRQ2lpqiY4n6Bb9oCS6lzDKVQxbZw=="],
"@types/node": ["@types/node@24.3.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g=="],
"@types/oboe": ["@types/oboe@2.1.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-bXt4BXSQy0N/buSIak1o0TjYAk2SAeK1aZV9xKcb+xVGWYP8NcMOFy2T7Um3kIvEcQJzrdgJ8R6fpbRcp/LEww=="],
@@ -2560,7 +2560,7 @@
"undici": ["undici@5.20.0", "", { "dependencies": { "busboy": "^1.6.0" } }, "sha512-J3j60dYzuo6Eevbawwp1sdg16k5Tf768bxYK4TUJRH7cBM4kFCbf3mOnM/0E3vQYXvpxITbbWmBafaDbxLDz3g=="],
"undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
"undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="],
"unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="],
@@ -3124,8 +3124,6 @@
"https-proxy-agent/debug": ["debug@4.3.5", "", { "dependencies": { "ms": "2.1.2" } }, "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg=="],
"image-q/@types/node": ["@types/node@16.9.1", "", {}, "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g=="],
"import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
"jest-diff/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],

View File

@@ -204,6 +204,7 @@ export async function makeTree(base: string, tree: DirectoryTree) {
makeTree(joined, contents);
continue;
}
contents;
fs.writeFileSync(joined, contents);
}
}
@@ -1252,6 +1253,7 @@ export async function runBunInstall(
stderr: "pipe",
env,
});
expect(stdout).toBeDefined();
expect(stderr).toBeDefined();
let err: string = stderrForInstall(await stderr.text());

View File

@@ -499,13 +499,13 @@ describe("@types/bun integration test", () => {
expect(emptyInterfaces).toEqual(expectedEmptyInterfacesWhenNoDOM);
expect(diagnostics).toEqual([
// This is expected because we, of course, can't check that our tsx file is passing
// when tsx is turned off...
{
"code": 17004,
"line": "[slug].tsx:17:10",
"message": "Cannot use JSX unless the '--jsx' flag is provided.",
},
// // This is expected because we, of course, can't check that our tsx file is passing
// // when tsx is turned off...
// {
// "code": 17004,
// "line": "[slug].tsx:17:10",
// "message": "Cannot use JSX unless the '--jsx' flag is provided.",
// },
]);
});

View File

@@ -1,38 +1,40 @@
import { join } from "path";
import { expectType } from "./utilities";
// import { join } from "path";
// import { expectType } from "./utilities";
// we're just checking types here really
declare function markdownToJSX(markdown: string): React.ReactNode;
// // we're just checking types here really
// declare function markdownToJSX(markdown: string): React.ReactNode;
type Params = {
slug: string;
};
// type Params = {
// slug: string;
// };
const Index: Bun.__experimental.SSGPage<Params> = async ({ params }) => {
expectType(params.slug).is<string>();
// const Index: Bun.__experimental.SSGPage<Params> = async ({ params }) => {
// expectType(params.slug).is<string>();
const content = await Bun.file(join(process.cwd(), "posts", params.slug + ".md")).text();
const node = markdownToJSX(content);
// const content = await Bun.file(join(process.cwd(), "posts", params.slug + ".md")).text();
// const node = markdownToJSX(content);
return <div>{node}</div>;
};
// return <div>{node}</div>;
// };
expectType(Index.displayName).is<string | undefined>();
// expectType(Index.displayName).is<string | undefined>();
export default Index;
// export default Index;
export const getStaticPaths: Bun.__experimental.GetStaticPaths<Params> = async () => {
const glob = new Bun.Glob("**/*.md");
const postsDir = join(process.cwd(), "posts");
const paths: Bun.__experimental.SSGPaths<Params> = [];
// export const getStaticPaths: Bun.__experimental.GetStaticPaths<Params> = async () => {
// const glob = new Bun.Glob("**/*.md");
// const postsDir = join(process.cwd(), "posts");
// const paths: Bun.__experimental.SSGPaths<Params> = [];
for (const file of glob.scanSync({ cwd: postsDir })) {
const slug = file.replace(/\.md$/, "");
// for (const file of glob.scanSync({ cwd: postsDir })) {
// const slug = file.replace(/\.md$/, "");
paths.push({
params: { slug },
});
}
// paths.push({
// params: { slug },
// });
// }
return { paths };
};
// return { paths };
// };
export {};

View File

@@ -26,22 +26,22 @@ function expectInstanceOf<T>(value: unknown, constructor: new (...args: any[]) =
expect(value).toBeInstanceOf(constructor);
}
function test<T = undefined, R extends string = never>(
function test<WebSocketData = undefined, R extends string = string>(
name: string,
options: Bun.Serve.Options<T, R>,
options: Bun.Serve.Options<WebSocketData, R>,
{
onConstructorFailure,
overrideExpectBehavior,
skip: skipOptions,
}: {
onConstructorFailure?: (error: Error) => void | Promise<void>;
overrideExpectBehavior?: (server: NoInfer<Bun.Server<T>>) => void | Promise<void>;
overrideExpectBehavior?: (server: NoInfer<Bun.Server<WebSocketData>>) => void | Promise<void>;
skip?: boolean;
} = {},
) {
const skip = skipOptions || ("unix" in options && typeof options.unix === "string" && process.platform === "win32");
async function testServer(server: Bun.Server<T>) {
async function testServer(server: Bun.Server<WebSocketData>) {
if (overrideExpectBehavior) {
await overrideExpectBehavior(server);
} else {
@@ -313,6 +313,13 @@ test(
},
);
test("with app", {
fetch: () => new Response("hello"),
app: {
framework: "bun-framework-react",
},
});
test(
"basic unix socket + upgrade + cheap request to check upgrade",
{

Some files were not shown because too many files have changed in this diff Show More