mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
Compare commits
364 Commits
bun-v1.3.5
...
ali/react
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e3ee654f2 | ||
|
|
e306ac831e | ||
|
|
ed1eb21093 | ||
|
|
4a4a37796b | ||
|
|
7aef153f5d | ||
|
|
3cb64478d7 | ||
|
|
7dfed6b986 | ||
|
|
5972cf24cb | ||
|
|
329c79364d | ||
|
|
da5bf73494 | ||
|
|
fbd58db004 | ||
|
|
81999c26e6 | ||
|
|
dde1f9e610 | ||
|
|
c9b5ba1c96 | ||
|
|
74f4fcf2a6 | ||
|
|
031f12442d | ||
|
|
a20a718e48 | ||
|
|
33537b6ec1 | ||
|
|
952436fc4c | ||
|
|
31352bc646 | ||
|
|
ff84564c11 | ||
|
|
cc78a1bca1 | ||
|
|
3ca89abacf | ||
|
|
2f02e4e31d | ||
|
|
efb508e2ae | ||
|
|
86af3dd034 | ||
|
|
a6d3808ad8 | ||
|
|
2153fe4163 | ||
|
|
6977b36215 | ||
|
|
ceaab9eda3 | ||
|
|
f262e32368 | ||
|
|
69a76d44f9 | ||
|
|
fc9538baf1 | ||
|
|
72b7956385 | ||
|
|
62b296bb43 | ||
|
|
cb14f70a43 | ||
|
|
38511375f8 | ||
|
|
06cfe2ead1 | ||
|
|
4f219503fe | ||
|
|
b2353d687e | ||
|
|
65e66099f7 | ||
|
|
c534f0caa0 | ||
|
|
a873152aeb | ||
|
|
a17eb07d48 | ||
|
|
1381de4d18 | ||
|
|
8d8b037e94 | ||
|
|
f43a175f72 | ||
|
|
e1ad16f857 | ||
|
|
661a246039 | ||
|
|
d0fed20c89 | ||
|
|
1c6165e68a | ||
|
|
daefbfb453 | ||
|
|
971e4679cf | ||
|
|
49a9dc7ddf | ||
|
|
b5a5fea9ae | ||
|
|
243a237a62 | ||
|
|
49cfda12a8 | ||
|
|
ab579a3cc3 | ||
|
|
8cd9b4eae6 | ||
|
|
dd9860f501 | ||
|
|
756e590782 | ||
|
|
c62613e765 | ||
|
|
7f96bf8f13 | ||
|
|
cd800b02f5 | ||
|
|
de999f78ab | ||
|
|
24748104ce | ||
|
|
3fca3b97d9 | ||
|
|
4cec2ecdc6 | ||
|
|
cdeb7bfb00 | ||
|
|
98b24f5797 | ||
|
|
678843fb59 | ||
|
|
7227745249 | ||
|
|
472e2d379f | ||
|
|
16360c9432 | ||
|
|
1b3d0d5c40 | ||
|
|
799248bfb4 | ||
|
|
a2689c03e9 | ||
|
|
2a7e2c9cf3 | ||
|
|
4f0d2a5624 | ||
|
|
23230112b0 | ||
|
|
c2c2a1685a | ||
|
|
09716704bb | ||
|
|
2d3223c5a6 | ||
|
|
891ea726d6 | ||
|
|
11eddb2cf1 | ||
|
|
0cc63255b1 | ||
|
|
71a5f9fb26 | ||
|
|
b257967189 | ||
|
|
4e629753cc | ||
|
|
7204820f19 | ||
|
|
af3a1ffd46 | ||
|
|
2ff068dad2 | ||
|
|
927065238b | ||
|
|
fa727b22de | ||
|
|
f20b0ced8e | ||
|
|
17fdb5bcdf | ||
|
|
f65d89ff8b | ||
|
|
c1931c11fe | ||
|
|
67d27499c3 | ||
|
|
85db75611b | ||
|
|
61519b320d | ||
|
|
c129d683cd | ||
|
|
0b0ffbf250 | ||
|
|
c64dd684c8 | ||
|
|
5aa5906ccf | ||
|
|
c93d8cf12b | ||
|
|
1a1091fd2c | ||
|
|
bdf77f968c | ||
|
|
457b4a46b3 | ||
|
|
3b2bea9820 | ||
|
|
5b4b99e2c4 | ||
|
|
da2be3f582 | ||
|
|
7282e92e48 | ||
|
|
80c28b6280 | ||
|
|
e40238fdc2 | ||
|
|
166e961202 | ||
|
|
58ecff4e0c | ||
|
|
a548ae7038 | ||
|
|
bbaabedce6 | ||
|
|
f84f90c09f | ||
|
|
43a7b6518a | ||
|
|
c85ab5218e | ||
|
|
bade403361 | ||
|
|
047eecc90c | ||
|
|
f03a1ab1c9 | ||
|
|
1e3057045c | ||
|
|
e92fd08930 | ||
|
|
deb3e94948 | ||
|
|
1b01f7c0da | ||
|
|
5e256e4b1f | ||
|
|
fc6fdbe300 | ||
|
|
247629aded | ||
|
|
2894e8d309 | ||
|
|
cc84e271ff | ||
|
|
c07150d5b1 | ||
|
|
b0d3815cf9 | ||
|
|
f145d8c30c | ||
|
|
3a23965581 | ||
|
|
0b45b9c29e | ||
|
|
9d679811cd | ||
|
|
cda3eb5396 | ||
|
|
b17dccc6e0 | ||
|
|
99a80a6fe6 | ||
|
|
8b7bc0fe59 | ||
|
|
7e89ca3d2f | ||
|
|
d8fa01ed41 | ||
|
|
361cd05676 | ||
|
|
dbe15d3020 | ||
|
|
3a200e8097 | ||
|
|
2701292a9f | ||
|
|
61b1aded3e | ||
|
|
36a414c087 | ||
|
|
ac02036879 | ||
|
|
612d41185b | ||
|
|
59b34efea8 | ||
|
|
243f3652f1 | ||
|
|
11dc2fae56 | ||
|
|
0919f237a5 | ||
|
|
691e731404 | ||
|
|
1a19be07ee | ||
|
|
903ac7bdd5 | ||
|
|
0ac6b17d4a | ||
|
|
921e3578b1 | ||
|
|
101bcb1ea0 | ||
|
|
f691ea1e96 | ||
|
|
53208e2538 | ||
|
|
53299d78b1 | ||
|
|
363c4a5c06 | ||
|
|
6e6120640e | ||
|
|
6556138c7b | ||
|
|
aea7b196e6 | ||
|
|
b3f92b0889 | ||
|
|
dab797b834 | ||
|
|
a8ff3f8ac3 | ||
|
|
b516eedc67 | ||
|
|
bcea163fd2 | ||
|
|
a47cbef4ca | ||
|
|
90e68fa095 | ||
|
|
da0b090834 | ||
|
|
e6aced6637 | ||
|
|
ce560cd318 | ||
|
|
e554c4e1ca | ||
|
|
731f42ca72 | ||
|
|
f33a852a80 | ||
|
|
f5122bdbf1 | ||
|
|
c29c69b9b5 | ||
|
|
916d44fc45 | ||
|
|
421a4f37cd | ||
|
|
d0da7076e6 | ||
|
|
ea78d564da | ||
|
|
6338d55f70 | ||
|
|
d3bdc77274 | ||
|
|
ecd2fed665 | ||
|
|
28447ab578 | ||
|
|
3e798f1787 | ||
|
|
a64f073ad3 | ||
|
|
bb19610f0d | ||
|
|
ed4a887047 | ||
|
|
894a654e26 | ||
|
|
99dd08bccb | ||
|
|
7339d1841b | ||
|
|
1217e87379 | ||
|
|
704661e96f | ||
|
|
8e659b2dc8 | ||
|
|
93007de396 | ||
|
|
2166f0c200 | ||
|
|
1a0a081e75 | ||
|
|
2eb33628d1 | ||
|
|
56e9c92b4a | ||
|
|
34cfdf039a | ||
|
|
d4a9c7a161 | ||
|
|
b5c16dcc1b | ||
|
|
0ba166eea3 | ||
|
|
1920a7c63c | ||
|
|
d56005b520 | ||
|
|
2bd5d68047 | ||
|
|
9c5c4edac4 | ||
|
|
cae0673dc4 | ||
|
|
51e18d379f | ||
|
|
199781bf4f | ||
|
|
ffeb21c49b | ||
|
|
d54ffd8012 | ||
|
|
dca34819b6 | ||
|
|
b4add533e6 | ||
|
|
7afcc8416f | ||
|
|
1ef578a0b4 | ||
|
|
8be4fb61d0 | ||
|
|
208ac7fb60 | ||
|
|
29b6faadf8 | ||
|
|
99df2e071f | ||
|
|
a3d91477a8 | ||
|
|
52c3e2e3f8 | ||
|
|
a7e95718ac | ||
|
|
db2960d27b | ||
|
|
bbfac709cc | ||
|
|
41fbeacee1 | ||
|
|
24b2929c9a | ||
|
|
bf992731c6 | ||
|
|
eafc04cc5d | ||
|
|
95cacdc6be | ||
|
|
6cf46e67f6 | ||
|
|
f28670ac68 | ||
|
|
0df21d7f30 | ||
|
|
39e7e55802 | ||
|
|
a63888ed6d | ||
|
|
e50385879b | ||
|
|
20d2f3805e | ||
|
|
d984f8f5ad | ||
|
|
9ea2ec876e | ||
|
|
0919e45c23 | ||
|
|
fd41a41ab9 | ||
|
|
fc06e1cf14 | ||
|
|
1778713cbf | ||
|
|
02d9da73bd | ||
|
|
6562275d15 | ||
|
|
cccae0cc79 | ||
|
|
c10d184448 | ||
|
|
0f8a232466 | ||
|
|
f47df15c18 | ||
|
|
f2d3141767 | ||
|
|
c8b21f207d | ||
|
|
6357978b90 | ||
|
|
151d8bb413 | ||
|
|
42bfccee3c | ||
|
|
f219a29248 | ||
|
|
b588512237 | ||
|
|
3a42ad8b1f | ||
|
|
cc1fff363d | ||
|
|
ba5e4784aa | ||
|
|
3e747886aa | ||
|
|
9504d14b7a | ||
|
|
911b670621 | ||
|
|
679282b8c6 | ||
|
|
1f79bc15a3 | ||
|
|
80a945f03f | ||
|
|
43054c9a7f | ||
|
|
2fad71dd45 | ||
|
|
6b2c3e61ea | ||
|
|
43d447f9fe | ||
|
|
8b35b5634a | ||
|
|
811f0888c8 | ||
|
|
a5d7830862 | ||
|
|
ef17dc57e4 | ||
|
|
e58cb4511e | ||
|
|
d44b3db1cb | ||
|
|
857e25d88c | ||
|
|
6eee2eeaf6 | ||
|
|
cc3e4d8319 | ||
|
|
278c2e7fb6 | ||
|
|
e296928ab9 | ||
|
|
39c1bf38f5 | ||
|
|
91d30b4da0 | ||
|
|
c2812fff79 | ||
|
|
e0337f5649 | ||
|
|
d05768cc18 | ||
|
|
1ad67908fc | ||
|
|
f55e320f41 | ||
|
|
8e0cf4c5e0 | ||
|
|
5dcf8a8076 | ||
|
|
d6b155f056 | ||
|
|
5f8393cc99 | ||
|
|
ee7dfefbe0 | ||
|
|
6d132e628f | ||
|
|
ae9ecc99c9 | ||
|
|
eeecbfa790 | ||
|
|
862f7378e4 | ||
|
|
636e597b60 | ||
|
|
6abb9f81eb | ||
|
|
aa33b11a7a | ||
|
|
21266f5263 | ||
|
|
c5fc729fde | ||
|
|
03d1e48004 | ||
|
|
19b9c4a850 | ||
|
|
842503ecb1 | ||
|
|
cb9c45c26c | ||
|
|
917dcc846f | ||
|
|
0fb277a56e | ||
|
|
c343aca21e | ||
|
|
e89a0f3807 | ||
|
|
59f12d30b3 | ||
|
|
f0d4fa8b63 | ||
|
|
3fb0a824cb | ||
|
|
ab3566627d | ||
|
|
3906407e5d | ||
|
|
33447ef2db | ||
|
|
3760407908 | ||
|
|
c1f0ce277d | ||
|
|
bfe3041179 | ||
|
|
5b6344cf3c | ||
|
|
b4fdf41ea5 | ||
|
|
b9da6b71f9 | ||
|
|
87487468f3 | ||
|
|
cfdeb42023 | ||
|
|
20e4c094ac | ||
|
|
17be416250 | ||
|
|
9745f01041 | ||
|
|
16131f92e1 | ||
|
|
59a4d0697b | ||
|
|
78a2ae44aa | ||
|
|
7f295919a9 | ||
|
|
1d0984b5c4 | ||
|
|
dfa93a8ede | ||
|
|
c8773c5e30 | ||
|
|
0f74fafc59 | ||
|
|
47d6e161fe | ||
|
|
160625c37c | ||
|
|
1b9b686772 | ||
|
|
6f3e098bac | ||
|
|
4c6b296a7c | ||
|
|
2ab962bf6b | ||
|
|
f556fc987c | ||
|
|
3a1b12ee61 | ||
|
|
a952b4200e | ||
|
|
24485fb432 | ||
|
|
b10fda0487 | ||
|
|
740cdaba3d | ||
|
|
68be15361a | ||
|
|
c57be8dcdb | ||
|
|
5115a88126 | ||
|
|
e992b804c8 | ||
|
|
b92555e099 | ||
|
|
381848cd69 | ||
|
|
61f9845f80 | ||
|
|
abc52da7bb |
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -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
|
||||
|
||||
3
bun.lock
3
bun.lock
@@ -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=="],
|
||||
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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",
|
||||
|
||||
1
packages/bun-framework-react/.gitignore
vendored
Normal file
1
packages/bun-framework-react/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
bun-framework-react*.tgz
|
||||
10
packages/bun-framework-react/README.md
Normal file
10
packages/bun-framework-react/README.md
Normal 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) 🎉
|
||||
32
packages/bun-framework-react/bun.lock
Normal file
32
packages/bun-framework-react/bun.lock
Normal 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=="],
|
||||
}
|
||||
}
|
||||
64
packages/bun-framework-react/client.tsx
Normal file
64
packages/bun-framework-react/client.tsx
Normal 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);
|
||||
});
|
||||
}
|
||||
35
packages/bun-framework-react/index.ts
Normal file
35
packages/bun-framework-react/index.ts
Normal 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;
|
||||
36
packages/bun-framework-react/package.json
Normal file
36
packages/bun-framework-react/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
96
packages/bun-framework-react/src/client/app.ts
Normal file
96
packages/bun-framework-react/src/client/app.ts
Normal 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);
|
||||
}
|
||||
3
packages/bun-framework-react/src/client/constants.ts
Normal file
3
packages/bun-framework-react/src/client/constants.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { Router } from "./router.ts";
|
||||
|
||||
export const router: Router = new Router();
|
||||
355
packages/bun-framework-react/src/client/css.ts
Normal file
355
packages/bun-framework-react/src/client/css.ts
Normal 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();
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
0
packages/bun-framework-react/src/client/entry.tsx
Normal file
0
packages/bun-framework-react/src/client/entry.tsx
Normal file
3
packages/bun-framework-react/src/client/lib/util.ts
Normal file
3
packages/bun-framework-react/src/client/lib/util.ts
Normal 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;
|
||||
}
|
||||
29
packages/bun-framework-react/src/client/root.tsx
Normal file
29
packages/bun-framework-react/src/client/root.tsx
Normal 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;
|
||||
}
|
||||
215
packages/bun-framework-react/src/client/router.ts
Normal file
215
packages/bun-framework-react/src/client/router.ts
Normal 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,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
39
packages/bun-framework-react/src/client/store.ts
Normal file
39
packages/bun-framework-react/src/client/store.ts
Normal 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);
|
||||
}
|
||||
31
packages/bun-framework-react/src/components/link.tsx
Normal file
31
packages/bun-framework-react/src/components/link.tsx
Normal 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);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
24
packages/bun-framework-react/tsconfig.json
Normal file
24
packages/bun-framework-react/tsconfig.json
Normal 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"
|
||||
}
|
||||
}
|
||||
1
packages/bun-framework-react/vendor/.prettierignore
vendored
Normal file
1
packages/bun-framework-react/vendor/.prettierignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*
|
||||
21
packages/bun-framework-react/vendor/react-server-dom-bun/LICENSE
vendored
Normal file
21
packages/bun-framework-react/vendor/react-server-dom-bun/LICENSE
vendored
Normal 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.
|
||||
5
packages/bun-framework-react/vendor/react-server-dom-bun/README.md
vendored
Normal file
5
packages/bun-framework-react/vendor/react-server-dom-bun/README.md
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
# react-server-dom-bun
|
||||
|
||||
Experimental React Flight bindings for DOM using Bun.
|
||||
|
||||
**Use it at your own risk.**
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1
packages/bun-framework-react/vendor/react-server-dom-bun/client.browser.d.ts
vendored
Normal file
1
packages/bun-framework-react/vendor/react-server-dom-bun/client.browser.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export function createFromReadableStream<T>(readable: ReadableStream<T>): Promise<T>;
|
||||
7
packages/bun-framework-react/vendor/react-server-dom-bun/client.browser.js
vendored
Normal file
7
packages/bun-framework-react/vendor/react-server-dom-bun/client.browser.js
vendored
Normal 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');
|
||||
}
|
||||
3
packages/bun-framework-react/vendor/react-server-dom-bun/client.js
vendored
Normal file
3
packages/bun-framework-react/vendor/react-server-dom-bun/client.js
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = require('./client.browser');
|
||||
7
packages/bun-framework-react/vendor/react-server-dom-bun/client.node.js
vendored
Normal file
7
packages/bun-framework-react/vendor/react-server-dom-bun/client.node.js
vendored
Normal 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');
|
||||
}
|
||||
20
packages/bun-framework-react/vendor/react-server-dom-bun/client.node.unbundled.d.ts
vendored
Normal file
20
packages/bun-framework-react/vendor/react-server-dom-bun/client.node.unbundled.d.ts
vendored
Normal 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>;
|
||||
7
packages/bun-framework-react/vendor/react-server-dom-bun/client.node.unbundled.js
vendored
Normal file
7
packages/bun-framework-react/vendor/react-server-dom-bun/client.node.unbundled.js
vendored
Normal 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');
|
||||
}
|
||||
3
packages/bun-framework-react/vendor/react-server-dom-bun/esm/package.json
vendored
Normal file
3
packages/bun-framework-react/vendor/react-server-dom-bun/esm/package.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "module"
|
||||
}
|
||||
12
packages/bun-framework-react/vendor/react-server-dom-bun/index.js
vendored
Normal file
12
packages/bun-framework-react/vendor/react-server-dom-bun/index.js
vendored
Normal 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.');
|
||||
91
packages/bun-framework-react/vendor/react-server-dom-bun/package.json
vendored
Normal file
91
packages/bun-framework-react/vendor/react-server-dom-bun/package.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
3
packages/bun-framework-react/vendor/react-server-dom-bun/plugin.js
vendored
Normal file
3
packages/bun-framework-react/vendor/react-server-dom-bun/plugin.js
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = require('./cjs/react-server-dom-bun-plugin.js');
|
||||
17
packages/bun-framework-react/vendor/react-server-dom-bun/server.browser.js
vendored
Normal file
17
packages/bun-framework-react/vendor/react-server-dom-bun/server.browser.js
vendored
Normal 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;
|
||||
6
packages/bun-framework-react/vendor/react-server-dom-bun/server.js
vendored
Normal file
6
packages/bun-framework-react/vendor/react-server-dom-bun/server.js
vendored
Normal 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.'
|
||||
);
|
||||
20
packages/bun-framework-react/vendor/react-server-dom-bun/server.node.js
vendored
Normal file
20
packages/bun-framework-react/vendor/react-server-dom-bun/server.node.js
vendored
Normal 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;
|
||||
23
packages/bun-framework-react/vendor/react-server-dom-bun/server.node.unbundled.d.ts
vendored
Normal file
23
packages/bun-framework-react/vendor/react-server-dom-bun/server.node.unbundled.d.ts
vendored
Normal 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;
|
||||
}
|
||||
20
packages/bun-framework-react/vendor/react-server-dom-bun/server.node.unbundled.js
vendored
Normal file
20
packages/bun-framework-react/vendor/react-server-dom-bun/server.node.unbundled.js
vendored
Normal 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;
|
||||
12
packages/bun-framework-react/vendor/react-server-dom-bun/static.browser.js
vendored
Normal file
12
packages/bun-framework-react/vendor/react-server-dom-bun/static.browser.js
vendored
Normal 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;
|
||||
}
|
||||
6
packages/bun-framework-react/vendor/react-server-dom-bun/static.js
vendored
Normal file
6
packages/bun-framework-react/vendor/react-server-dom-bun/static.js
vendored
Normal 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.'
|
||||
);
|
||||
15
packages/bun-framework-react/vendor/react-server-dom-bun/static.node.js
vendored
Normal file
15
packages/bun-framework-react/vendor/react-server-dom-bun/static.node.js
vendored
Normal 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;
|
||||
}
|
||||
12
packages/bun-framework-react/vendor/react-server-dom-bun/static.node.unbundled.js
vendored
Normal file
12
packages/bun-framework-react/vendor/react-server-dom-bun/static.node.unbundled.js
vendored
Normal 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
1550
packages/bun-types/app.d.ts
vendored
Normal file
File diff suppressed because it is too large
Load Diff
278
packages/bun-types/experimental.d.ts
vendored
278
packages/bun-types/experimental.d.ts
vendored
@@ -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>;
|
||||
}>;
|
||||
}
|
||||
}
|
||||
2
packages/bun-types/index.d.ts
vendored
2
packages/bun-types/index.d.ts
vendored
@@ -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" />
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
234
src/bake.zig
234
src/bake.zig
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
605
src/bake/bake.d.ts
vendored
@@ -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" {}
|
||||
89
src/bake/bake.private.d.ts
vendored
89
src/bake/bake.private.d.ts
vendored
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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"],
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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" }]
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()];
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
|
||||
26
src/cli.zig
26
src/cli.zig
@@ -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")) {
|
||||
|
||||
@@ -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
|
||||
|
||||
3
src/js/builtins.d.ts
vendored
3
src/js/builtins.d.ts
vendored
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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=="],
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// @ts-nocheck
|
||||
import { createRoot } from "react-dom/client";
|
||||
import App from "./App";
|
||||
createRoot(document.getElementById("root")!).render(<App />);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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=="],
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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.",
|
||||
// },
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -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 {};
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user