Compare commits
123
Commits
cf010ad4f5
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1bf1b651db | ||
|
|
247a64c873 | ||
|
|
f590deb11b | ||
|
|
3426c7b53b | ||
|
|
afd1af1827 | ||
|
|
22d3dedab2 | ||
|
|
8d12329f01 | ||
|
|
78e86b3999 | ||
|
|
87948340b0 | ||
|
|
86ea86e7bd | ||
|
|
596b9de047 | ||
|
|
9da1e16318 | ||
|
|
823c5bb1ca | ||
|
|
88ead3b3d6 | ||
|
|
45509deff8 | ||
|
|
a19863b3e9 | ||
|
|
a9cd51d71c | ||
|
|
e3a0f5fbeb | ||
|
|
1d0ac4aa80 | ||
|
|
817beacc7a | ||
|
|
61a9d8d612 | ||
|
|
cd9ac833dc | ||
|
|
e2b8b95a4b | ||
|
|
c06cb73181 | ||
|
|
f3bb972a20 | ||
|
|
40a987b983 | ||
|
|
ceeb6fbdaf | ||
|
|
4f5445e3df | ||
|
|
b702e26bf6 | ||
|
|
3b26292124 | ||
|
|
cb5126c947 | ||
|
|
f018b228a8 | ||
|
|
95fcee36b2 | ||
|
|
41e28d7a3e | ||
|
|
b2e92e5792 | ||
|
|
9a427412f3 | ||
|
|
a5f25d522e | ||
|
|
938411653d | ||
|
|
b597e1420e | ||
|
|
d6f0a7962e | ||
|
|
f0aae8a8fa | ||
|
|
84d490b189 | ||
|
|
532d0d6b56 | ||
|
|
e0c2ab5872 | ||
|
|
35f7a6fc85 | ||
|
|
814a289460 | ||
|
|
c4f7cf9164 | ||
|
|
af8c207ac1 | ||
|
|
5a0972beb8 | ||
|
|
257adfec39 | ||
|
|
349b99bacf | ||
|
|
972f6945c2 | ||
|
|
6477f446d1 | ||
|
|
7fb1784b95 | ||
|
|
40fc7778a8 | ||
|
|
f3b087a1f3 | ||
|
|
eb0721f94b | ||
|
|
ab6fc3be26 | ||
|
|
6b7d29bc53 | ||
|
|
a84ac10b24 | ||
|
|
4b3a4dbc77 | ||
|
|
c19b7c9607 | ||
|
|
57957b89a4 | ||
|
|
d441f9982e | ||
|
|
f49a17b24e | ||
|
|
fa8a108686 | ||
|
|
8d5b2a5c07 | ||
|
|
333c84281d | ||
|
|
5166ba037f | ||
|
|
1037bbb546 | ||
|
|
f34f8baa99 | ||
|
|
48c25d2fb4 | ||
|
|
7dbd1f7e12 | ||
|
|
68d88bce4a | ||
|
|
d6985b3335 | ||
|
|
86d4bb8d6c | ||
|
|
2b170d24e1 | ||
|
|
950bdc3dca | ||
|
|
572f739b5c | ||
|
|
ee07163ba5 | ||
|
|
ad51f7da0c | ||
|
|
6a024faece | ||
|
|
764e8f76f0 | ||
|
|
b8bae51b32 | ||
|
|
dad5cffaac | ||
|
|
321c8ebb89 | ||
|
|
336fa812a5 | ||
|
|
f6c7f6b78c | ||
|
|
99b319f5af | ||
|
|
47c262392c | ||
|
|
976ff5fec9 | ||
|
|
60ee353bf0 | ||
|
|
342f56f3f5 | ||
|
|
7dd8d87be8 | ||
|
|
6f1db7c01f | ||
|
|
67ed8749e2 | ||
|
|
146f51c590 | ||
|
|
4b8b610e32 | ||
|
|
567fb4f746 | ||
|
|
0308ea32ec | ||
|
|
1634eeb213 | ||
|
|
e047539a11 | ||
|
|
e0a07cf57c | ||
|
|
e2df3256f4 | ||
|
|
c3e1341ff8 | ||
|
|
49d6781170 | ||
|
|
3d044ee79e | ||
|
|
a9af20a8b2 | ||
|
|
9f619c8898 | ||
|
|
32056b5cb6 | ||
|
|
a504046a36 | ||
|
|
5ebac74353 | ||
|
|
070e6860d8 | ||
|
|
d75125d9fa | ||
|
|
4ee3876710 | ||
|
|
632c70fc90 | ||
|
|
13bd8bb851 | ||
|
|
4b79993d37 | ||
|
|
51db0c447c | ||
|
|
a38841a587 | ||
|
|
d3336b48c7 | ||
|
|
3545b6dc08 | ||
|
|
d2ddaef0d4 |
+8
-1
@@ -175,4 +175,11 @@ out
|
||||
/test
|
||||
.bunext
|
||||
/bin
|
||||
/build
|
||||
/build
|
||||
__fixtures__
|
||||
/public
|
||||
/.data
|
||||
/.dump
|
||||
/.vscode
|
||||
/source.md
|
||||
SECURITY.md
|
||||
@@ -0,0 +1,2 @@
|
||||
@moduletrace:registry=https://git.tben.me/api/packages/moduletrace/npm/
|
||||
//git.tben.me/api/packages/moduletrace/npm/:_authToken=${GITBEN_NPM_TOKEN}
|
||||
@@ -40,11 +40,21 @@ bunext start # Start production server from pre-built artifacts
|
||||
- `src/presets/` — Default 404/500 components and sample `bunext.config.ts`
|
||||
|
||||
### Page module contract
|
||||
Pages live in `src/pages/`. A page file may export:
|
||||
- Default export: React component receiving `ServerProps | StaticProps`
|
||||
- `server`: `BunextPageServerFn` — runs server-side before rendering, return value becomes props
|
||||
Pages live in `src/pages/`. Server logic is **separated from page files** into companion `.server.ts` / `.server.tsx` files to avoid bundling server-only code into the client.
|
||||
|
||||
**Page file** (`page.tsx`) — client-safe exports only (bundled to the browser):
|
||||
- Default export: React component receiving props from the server file
|
||||
- `meta`: `BunextPageModuleMeta` — SEO/OG metadata
|
||||
- `head`: ReactNode — extra `<head>` content
|
||||
- `Head`: FC — extra `<head>` content
|
||||
- `config`: `BunextRouteConfig` — cache settings
|
||||
- `html_props`: `BunextHTMLProps` — attributes on the `<html>` element
|
||||
|
||||
**Server file** (`page.server.ts` or `page.server.tsx`) — server-only, never sent to the browser:
|
||||
- Default export or `export const server`: `BunextPageServerFn` — runs server-side before rendering, return value becomes props
|
||||
|
||||
The framework resolves the companion by replacing the page extension with `.server.ts` or `.server.tsx`. If neither exists, no server function runs and only the default `url` prop is injected.
|
||||
|
||||
`__root.tsx` follows the same contract; its server companion is `__root.server.ts`.
|
||||
|
||||
API routes live in `src/pages/api/` and follow standard Bun `Request → Response` handler conventions.
|
||||
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "bun-next",
|
||||
"dependencies": {
|
||||
"@tailwindcss/postcss": "^4.2.2",
|
||||
"@types/bun": "latest",
|
||||
"@types/node": "^24.10.0",
|
||||
"@types/react": "^19.2.2",
|
||||
"@types/react-dom": "^19.2.2",
|
||||
"bun-plugin-tailwind": "^0.1.2",
|
||||
"chalk": "^5.6.2",
|
||||
"chokidar": "^5.0.0",
|
||||
"commander": "^14.0.2",
|
||||
"esbuild": "^0.27.4",
|
||||
"lightningcss-wasm": "^1.32.0",
|
||||
@@ -14,27 +20,29 @@
|
||||
"micromatch": "^4.0.8",
|
||||
"ora": "^9.0.0",
|
||||
"postcss": "^8.5.8",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
"@types/lodash": "^4.17.24",
|
||||
"@types/micromatch": "^4.0.10",
|
||||
"@types/node": "^24.10.0",
|
||||
"@types/react": "^19.2.2",
|
||||
"@types/react-dom": "^19.2.2",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"typescript": "^5.0.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@types/chokidar": "^2.1.7",
|
||||
"@types/lodash": "^4.17.24",
|
||||
"@types/micromatch": "^4.0.10",
|
||||
"happy-dom": "^20.8.4",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
|
||||
|
||||
"@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
|
||||
|
||||
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
|
||||
|
||||
"@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="],
|
||||
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q=="],
|
||||
|
||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.27.4", "", { "os": "android", "cpu": "arm" }, "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ=="],
|
||||
@@ -151,9 +159,15 @@
|
||||
|
||||
"@tailwindcss/postcss": ["@tailwindcss/postcss@4.2.2", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.2.2", "@tailwindcss/oxide": "4.2.2", "postcss": "^8.5.6", "tailwindcss": "4.2.2" } }, "sha512-n4goKQbW8RVXIbNKRB/45LzyUqN451deQK0nzIeauVEqjlI49slUlgKYJM2QyUzap/PcpnS7kzSUmPb1sCRvYQ=="],
|
||||
|
||||
"@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="],
|
||||
|
||||
"@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="],
|
||||
|
||||
"@types/braces": ["@types/braces@3.0.5", "", {}, "sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.2.3", "", { "dependencies": { "bun-types": "1.2.3" } }, "sha512-054h79ipETRfjtsCW9qJK8Ipof67Pw9bodFWmkfkaUaRiIQ1dIV2VTlheshlBx3mpKr0KeK8VqnMMCtgN9rQtw=="],
|
||||
"@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="],
|
||||
|
||||
"@types/chokidar": ["@types/chokidar@2.1.7", "", { "dependencies": { "chokidar": "*" } }, "sha512-A7/MFHf6KF7peCzjEC1BBTF8jpmZTokb3vr/A0NxRGfwRLK3Ws+Hq6ugVn6cJIMfM6wkCak/aplWrxbTcu8oig=="],
|
||||
|
||||
"@types/lodash": ["@types/lodash@4.17.24", "", {}, "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ=="],
|
||||
|
||||
@@ -165,9 +179,15 @@
|
||||
|
||||
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
||||
|
||||
"@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="],
|
||||
"@types/whatwg-mimetype": ["@types/whatwg-mimetype@3.0.2", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="],
|
||||
|
||||
"ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
|
||||
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
|
||||
|
||||
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
|
||||
|
||||
"aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="],
|
||||
|
||||
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||
|
||||
@@ -175,10 +195,12 @@
|
||||
|
||||
"bun-plugin-tailwind": ["bun-plugin-tailwind@0.1.2", "", { "peerDependencies": { "bun": ">=1.0.0" } }, "sha512-41jNC1tZRSK3s1o7pTNrLuQG8kL/0vR/JgiTmZAJ1eHwe0w5j6HFPKeqEk0WAD13jfrUC7+ULuewFBBCoADPpg=="],
|
||||
|
||||
"bun-types": ["bun-types@1.2.3", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-P7AeyTseLKAvgaZqQrvp3RqFM3yN9PlcLuSTe7SoJOfZkER73mLdT2vEQi8U64S1YvM/ldcNiQjn0Sn7H9lGgg=="],
|
||||
"bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="],
|
||||
|
||||
"chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
|
||||
|
||||
"chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
|
||||
|
||||
"cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="],
|
||||
|
||||
"cli-spinners": ["cli-spinners@3.3.0", "", {}, "sha512-/+40ljC3ONVnYIttjMWrlL51nItDAbBrq2upN8BPyvGU/2n5Oxw3tbNwORCaNuNqLJnxGqOfjUuhsv7l5Q4IsQ=="],
|
||||
@@ -187,10 +209,16 @@
|
||||
|
||||
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||
|
||||
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
|
||||
|
||||
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="],
|
||||
|
||||
"enhanced-resolve": ["enhanced-resolve@5.20.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA=="],
|
||||
|
||||
"entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
|
||||
|
||||
"esbuild": ["esbuild@0.27.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.4", "@esbuild/android-arm": "0.27.4", "@esbuild/android-arm64": "0.27.4", "@esbuild/android-x64": "0.27.4", "@esbuild/darwin-arm64": "0.27.4", "@esbuild/darwin-x64": "0.27.4", "@esbuild/freebsd-arm64": "0.27.4", "@esbuild/freebsd-x64": "0.27.4", "@esbuild/linux-arm": "0.27.4", "@esbuild/linux-arm64": "0.27.4", "@esbuild/linux-ia32": "0.27.4", "@esbuild/linux-loong64": "0.27.4", "@esbuild/linux-mips64el": "0.27.4", "@esbuild/linux-ppc64": "0.27.4", "@esbuild/linux-riscv64": "0.27.4", "@esbuild/linux-s390x": "0.27.4", "@esbuild/linux-x64": "0.27.4", "@esbuild/netbsd-arm64": "0.27.4", "@esbuild/netbsd-x64": "0.27.4", "@esbuild/openbsd-arm64": "0.27.4", "@esbuild/openbsd-x64": "0.27.4", "@esbuild/openharmony-arm64": "0.27.4", "@esbuild/sunos-x64": "0.27.4", "@esbuild/win32-arm64": "0.27.4", "@esbuild/win32-ia32": "0.27.4", "@esbuild/win32-x64": "0.27.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ=="],
|
||||
|
||||
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
|
||||
@@ -199,6 +227,8 @@
|
||||
|
||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||
|
||||
"happy-dom": ["happy-dom@20.8.4", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.18.3" } }, "sha512-GKhjq4OQCYB4VLFBzv8mmccUadwlAusOZOI7hC1D9xDIT5HhzkJK17c4el2f6R6C715P9xB4uiMxeKUa2nHMwQ=="],
|
||||
|
||||
"is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="],
|
||||
|
||||
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
|
||||
@@ -207,6 +237,8 @@
|
||||
|
||||
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
|
||||
|
||||
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
|
||||
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
|
||||
|
||||
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
|
||||
@@ -237,6 +269,8 @@
|
||||
|
||||
"log-symbols": ["log-symbols@7.0.1", "", { "dependencies": { "is-unicode-supported": "^2.0.0", "yoctocolors": "^2.1.1" } }, "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg=="],
|
||||
|
||||
"lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="],
|
||||
|
||||
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||
|
||||
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
||||
@@ -255,10 +289,16 @@
|
||||
|
||||
"postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
|
||||
|
||||
"pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="],
|
||||
|
||||
"react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
|
||||
|
||||
"react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="],
|
||||
|
||||
"react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="],
|
||||
|
||||
"readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="],
|
||||
|
||||
"restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="],
|
||||
|
||||
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||
@@ -283,6 +323,10 @@
|
||||
|
||||
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||
|
||||
"whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="],
|
||||
|
||||
"ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="],
|
||||
|
||||
"yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.9.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA=="],
|
||||
@@ -297,14 +341,8 @@
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"@types/ws/@types/node": ["@types/node@22.13.5", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg=="],
|
||||
|
||||
"bun-types/@types/node": ["@types/node@22.13.5", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg=="],
|
||||
|
||||
"lightningcss-wasm/napi-wasm": ["napi-wasm@1.1.3", "", { "bundled": true }, "sha512-h/4nMGsHjZDCYmQVNODIrYACVJ+I9KItbG+0si6W/jSjdA9JbWDoU4LLeMXVcEQGHjttI2tuXqDrbGF7qkUHHg=="],
|
||||
|
||||
"@types/ws/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
|
||||
|
||||
"bun-types/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
|
||||
"strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
|
||||
}
|
||||
}
|
||||
|
||||
+28
-12
@@ -58,7 +58,7 @@ This report compares the two on their overlapping surface — server-side render
|
||||
| Router | `Bun.FileSystemRouter` | Custom (Pages Router) / React Router (App Router) |
|
||||
| SSR method | `renderToString` (complete response, by design) | `renderToReadableStream` (streaming) |
|
||||
| Component model | Classic SSR + hydration | React Server Components + Client Components |
|
||||
| Data fetching | Per-page `server` export | `getServerSideProps`, `getStaticProps`, `fetch` in RSC |
|
||||
| Data fetching | Per-page `.server.ts` companion file | `getServerSideProps`, `getStaticProps`, `fetch` in RSC |
|
||||
| State persistence | `window.__PAGE_PROPS__` | RSC payload, router cache |
|
||||
| Dev HMR transport | Server-Sent Events (SSE) | WebSocket |
|
||||
| Config format | `bunext.config.ts` | `next.config.js` / `next.config.ts` |
|
||||
@@ -210,19 +210,25 @@ Streaming SSR's benefits — progressive flushing, Suspense-based partial render
|
||||
|
||||
### Data Fetching
|
||||
|
||||
**Bunext** exposes a single data-fetching primitive: the `server` export on each page module.
|
||||
**Bunext** exposes a single data-fetching primitive: a companion **`.server.ts`** file alongside each page.
|
||||
|
||||
```ts
|
||||
export const server: BunextPageServerFn = async (ctx) => {
|
||||
// src/pages/products.server.ts
|
||||
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
|
||||
|
||||
const server: BunextPageServerFn = async (ctx) => {
|
||||
const data = await db.query(...);
|
||||
return { props: { data } };
|
||||
};
|
||||
|
||||
export default server;
|
||||
```
|
||||
|
||||
The return value is serialized to `window.__PAGE_PROPS__` and passed as component props. A `url` object (copy of the request `URL`) is **always** injected into server props as a default, so every page can read URL metadata without writing a server function.
|
||||
The server file is never bundled into client JS — it runs exclusively on the server at request time. The return value is serialized to `window.__PAGE_PROPS__` and passed as component props. A `url` object (copy of the request `URL`) is **always** injected into server props as a default, so every page can read URL metadata without writing a server file at all.
|
||||
|
||||
**Design notes:**
|
||||
- One server function per page. Data fetching is centralised at the page level, not scattered across components.
|
||||
- One server file per page. Data fetching is centralised at the page level, not scattered across components.
|
||||
- The page file (`.tsx`) exports only client-safe code — the React component, `meta`, `Head`, `config`, and `html_props`. Server-only code (Bun APIs, database clients, `fs`) lives in the `.server.ts` companion and is never sent to the browser.
|
||||
- All rendering is on-demand. SSG is intentionally out of scope — see [Caching](#caching) for how Bunext addresses this differently.
|
||||
- Server function result is passed via `window.__PAGE_PROPS__`, serialized to JSON and embedded in the HTML — large payloads increase page size.
|
||||
|
||||
@@ -289,7 +295,7 @@ Bunext's caching model is its answer to SSG. Rather than pre-building pages at d
|
||||
|
||||
**Bunext** implements a **file-based HTML cache**:
|
||||
|
||||
- Enabled per-page via `config.cachePage` or returned dynamically from the `server` function at runtime.
|
||||
- Enabled per-page via `config.cachePage` (exported from the page file) or returned dynamically from the server function in the `.server.ts` companion at runtime.
|
||||
- On a cache miss, the rendered HTML is written to `public/__bunext/cache/<key>.res.html` alongside a metadata file `<key>.meta.json` (creation timestamp, expiry, paradigm).
|
||||
- On a cache hit, the HTML file is read and returned with `X-Bunext-Cache: HIT`.
|
||||
- A cron job runs every 30 seconds to delete expired entries.
|
||||
@@ -316,7 +322,7 @@ The key distinction from SSG: Bunext's cache is **demand-driven**. A site with 1
|
||||
|
||||
### Metadata and SEO
|
||||
|
||||
**Bunext** supports both static and dynamic metadata:
|
||||
**Bunext** supports both static and dynamic metadata. These exports live in the **page file** (not the `.server.ts` companion), since they are processed at the server HTML-generation step and may reference types from the client module:
|
||||
|
||||
- `export const meta: BunextPageModuleMeta` — static object with `title`, `description`, `keywords`, `author`, `robots`, `canonical`, `themeColor`, `og.*`, and `twitter.*` fields.
|
||||
- `export const meta: BunextPageModuleMetaFn` — async function receiving `ctx` and `serverRes` for dynamic metadata based on fetched data.
|
||||
@@ -540,10 +546,13 @@ Next.js wraps these in `NextRequest` and `NextResponse`, which add convenience m
|
||||
|
||||
### Conditional Runtime Caching
|
||||
|
||||
Bunext's `server` function can return `cachePage: true` and `cacheExpiry: N` based on runtime data — the authenticated state of the user, A/B test bucket, content freshness, or any other request-time condition:
|
||||
Bunext's server function can return `cachePage: true` and `cacheExpiry: N` based on runtime data — the authenticated state of the user, A/B test bucket, content freshness, or any other request-time condition:
|
||||
|
||||
```ts
|
||||
export const server: BunextPageServerFn = async (ctx) => {
|
||||
// src/pages/products.server.ts
|
||||
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
|
||||
|
||||
const server: BunextPageServerFn = async (ctx) => {
|
||||
const user = await getUser(ctx.req);
|
||||
return {
|
||||
props: { data },
|
||||
@@ -551,16 +560,21 @@ export const server: BunextPageServerFn = async (ctx) => {
|
||||
cacheExpiry: 300,
|
||||
};
|
||||
};
|
||||
|
||||
export default server;
|
||||
```
|
||||
|
||||
Next.js's ISR and full-route cache operate on fixed revalidation intervals set at build time. There is no mechanism to decide at runtime whether a specific request should be cached.
|
||||
|
||||
### Page Response Transform
|
||||
|
||||
The `resTransform` field in the page `server` function's `ctx` parameter lets the developer post-process the final HTML response generated by the framework — add headers, set cookies, modify status codes — without touching middleware:
|
||||
The `resTransform` field in the server function's `ctx` parameter lets the developer post-process the final HTML response generated by the framework — add headers, set cookies, modify status codes — without touching middleware:
|
||||
|
||||
```ts
|
||||
export const server: BunextPageServerFn = async (ctx) => {
|
||||
// src/pages/page.server.ts
|
||||
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
|
||||
|
||||
const server: BunextPageServerFn = async (ctx) => {
|
||||
ctx.resTransform = (res) => {
|
||||
res.headers.set("X-Custom-Header", "value");
|
||||
return res;
|
||||
@@ -568,6 +582,8 @@ export const server: BunextPageServerFn = async (ctx) => {
|
||||
|
||||
return { props: {} };
|
||||
};
|
||||
|
||||
export default server;
|
||||
```
|
||||
|
||||
This exists because page responses are generated entirely by the framework (`renderToString` → HTML template). Unlike API routes — where the developer returns a `Response` directly and already has full control — there is no other hook to modify the final page response at the route level without going through global middleware.
|
||||
@@ -588,7 +604,7 @@ Next.js's codebase spans Turbopack (Rust), SWC (Rust), the App Router internals,
|
||||
|
||||
The RSC model requires developers to constantly reason about the `"use client"` / `"use server"` boundary: what can be async, what has access to browser APIs, what gets serialized into the RSC payload. Mistakes at this boundary produce runtime errors that are difficult to diagnose.
|
||||
|
||||
Bunext has one rule: the `server` function runs on the server, the component runs on both (SSR then hydration). There is no boundary to reason about.
|
||||
Bunext has one rule: the `.server.ts` companion file runs on the server, the page file runs on both (SSR then hydration). The separation is enforced by the file system, not by decorators or directives. There is no boundary to reason about inside a file.
|
||||
|
||||
### No Vendor Lock-In
|
||||
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import { describe, it, expect } from "bun:test";
|
||||
import grabCacheNames from "../../../functions/cache/grab-cache-names";
|
||||
describe("grabCacheNames", () => {
|
||||
it("returns cache_name and cache_meta_name for a simple key", () => {
|
||||
const { cache_name, cache_meta_name } = grabCacheNames({ key: "home" });
|
||||
expect(cache_name).toBe("home.res.html");
|
||||
expect(cache_meta_name).toBe("home.meta.json");
|
||||
});
|
||||
it("defaults paradigm to html", () => {
|
||||
const { cache_name } = grabCacheNames({ key: "page" });
|
||||
expect(cache_name).toEndWith(".res.html");
|
||||
});
|
||||
it("uses json paradigm when specified", () => {
|
||||
const { cache_name } = grabCacheNames({ key: "api-data", paradigm: "json" });
|
||||
expect(cache_name).toBe("api-data.res.json");
|
||||
});
|
||||
it("URL-encodes the key", () => {
|
||||
const { cache_name, cache_meta_name } = grabCacheNames({
|
||||
key: "/blog/hello world",
|
||||
});
|
||||
const encoded = encodeURIComponent("/blog/hello world");
|
||||
expect(cache_name).toBe(`${encoded}.res.html`);
|
||||
expect(cache_meta_name).toBe(`${encoded}.meta.json`);
|
||||
});
|
||||
it("handles keys with special characters", () => {
|
||||
const key = "page?id=1&sort=asc";
|
||||
const { cache_name } = grabCacheNames({ key });
|
||||
expect(cache_name).toBe(`${encodeURIComponent(key)}.res.html`);
|
||||
});
|
||||
it("cache_meta_name always uses .meta.json regardless of paradigm", () => {
|
||||
const { cache_meta_name } = grabCacheNames({
|
||||
key: "test",
|
||||
paradigm: "json",
|
||||
});
|
||||
expect(cache_meta_name).toBe("test.meta.json");
|
||||
});
|
||||
});
|
||||
@@ -1,97 +0,0 @@
|
||||
import { describe, it, expect } from "bun:test";
|
||||
import grabWebMetaHTML from "../../../functions/server/web-pages/grab-web-meta-html";
|
||||
describe("grabWebMetaHTML", () => {
|
||||
it("returns empty string for empty meta object", () => {
|
||||
expect(grabWebMetaHTML({ meta: {} })).toBe("");
|
||||
});
|
||||
it("generates a title tag", () => {
|
||||
const html = grabWebMetaHTML({ meta: { title: "My Page" } });
|
||||
expect(html).toContain("<title>My Page</title>");
|
||||
});
|
||||
it("generates a description meta tag", () => {
|
||||
const html = grabWebMetaHTML({ meta: { description: "A description" } });
|
||||
expect(html).toContain('<meta name="description" content="A description"');
|
||||
});
|
||||
it("joins array keywords with comma", () => {
|
||||
const html = grabWebMetaHTML({
|
||||
meta: { keywords: ["react", "bun", "ssr"] },
|
||||
});
|
||||
expect(html).toContain('content="react, bun, ssr"');
|
||||
});
|
||||
it("uses string keywords directly", () => {
|
||||
const html = grabWebMetaHTML({ meta: { keywords: "react, bun" } });
|
||||
expect(html).toContain('content="react, bun"');
|
||||
});
|
||||
it("generates author meta tag", () => {
|
||||
const html = grabWebMetaHTML({ meta: { author: "Alice" } });
|
||||
expect(html).toContain('<meta name="author" content="Alice"');
|
||||
});
|
||||
it("generates robots meta tag", () => {
|
||||
const html = grabWebMetaHTML({ meta: { robots: "noindex" } });
|
||||
expect(html).toContain('<meta name="robots" content="noindex"');
|
||||
});
|
||||
it("generates canonical link tag", () => {
|
||||
const html = grabWebMetaHTML({
|
||||
meta: { canonical: "https://example.com/page" },
|
||||
});
|
||||
expect(html).toContain('<link rel="canonical" href="https://example.com/page"');
|
||||
});
|
||||
it("generates theme-color meta tag", () => {
|
||||
const html = grabWebMetaHTML({ meta: { themeColor: "#ff0000" } });
|
||||
expect(html).toContain('<meta name="theme-color" content="#ff0000"');
|
||||
});
|
||||
it("generates OG tags", () => {
|
||||
const html = grabWebMetaHTML({
|
||||
meta: {
|
||||
og: {
|
||||
title: "OG Title",
|
||||
description: "OG Desc",
|
||||
image: "https://example.com/img.png",
|
||||
url: "https://example.com",
|
||||
type: "website",
|
||||
siteName: "Example",
|
||||
locale: "en_US",
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(html).toContain('<meta property="og:title" content="OG Title"');
|
||||
expect(html).toContain('<meta property="og:description" content="OG Desc"');
|
||||
expect(html).toContain('<meta property="og:image" content="https://example.com/img.png"');
|
||||
expect(html).toContain('<meta property="og:url" content="https://example.com"');
|
||||
expect(html).toContain('<meta property="og:type" content="website"');
|
||||
expect(html).toContain('<meta property="og:site_name" content="Example"');
|
||||
expect(html).toContain('<meta property="og:locale" content="en_US"');
|
||||
});
|
||||
it("generates Twitter card tags", () => {
|
||||
const html = grabWebMetaHTML({
|
||||
meta: {
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: "Tweet Title",
|
||||
description: "Tweet Desc",
|
||||
image: "https://example.com/tw.png",
|
||||
site: "@example",
|
||||
creator: "@alice",
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(html).toContain('<meta name="twitter:card" content="summary_large_image"');
|
||||
expect(html).toContain('<meta name="twitter:title" content="Tweet Title"');
|
||||
expect(html).toContain('<meta name="twitter:description" content="Tweet Desc"');
|
||||
expect(html).toContain('<meta name="twitter:image" content="https://example.com/tw.png"');
|
||||
expect(html).toContain('<meta name="twitter:site" content="@example"');
|
||||
expect(html).toContain('<meta name="twitter:creator" content="@alice"');
|
||||
});
|
||||
it("skips undefined OG fields", () => {
|
||||
const html = grabWebMetaHTML({ meta: { og: { title: "Only Title" } } });
|
||||
expect(html).toContain("og:title");
|
||||
expect(html).not.toContain("og:description");
|
||||
expect(html).not.toContain("og:image");
|
||||
});
|
||||
it("does not emit tags for missing fields", () => {
|
||||
const html = grabWebMetaHTML({ meta: { title: "Hello" } });
|
||||
expect(html).not.toContain("description");
|
||||
expect(html).not.toContain("og:");
|
||||
expect(html).not.toContain("twitter:");
|
||||
});
|
||||
});
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
import { describe, it, expect } from "bun:test";
|
||||
import deserializeQuery from "../../utils/deserialize-query";
|
||||
describe("deserializeQuery", () => {
|
||||
it("passes through a plain object unchanged", () => {
|
||||
const input = { foo: "bar" };
|
||||
expect(deserializeQuery(input)).toEqual({ foo: "bar" });
|
||||
});
|
||||
it("parses a JSON string into an object", () => {
|
||||
const input = JSON.stringify({ a: 1, b: "hello" });
|
||||
expect(deserializeQuery(input)).toEqual({ a: 1, b: "hello" });
|
||||
});
|
||||
it("deep-parses string values that look like JSON objects", () => {
|
||||
const nested = { filter: JSON.stringify({ status: "active" }) };
|
||||
const result = deserializeQuery(nested);
|
||||
expect(result.filter).toEqual({ status: "active" });
|
||||
});
|
||||
it("deep-parses string values that look like JSON arrays", () => {
|
||||
const nested = { ids: JSON.stringify([1, 2, 3]) };
|
||||
const result = deserializeQuery(nested);
|
||||
expect(result.ids).toEqual([1, 2, 3]);
|
||||
});
|
||||
it("leaves plain string values alone", () => {
|
||||
const input = { name: "alice", age: "30" };
|
||||
expect(deserializeQuery(input)).toEqual({ name: "alice", age: "30" });
|
||||
});
|
||||
it("returns an empty object for an empty JSON string", () => {
|
||||
expect(deserializeQuery("{}")).toEqual({});
|
||||
});
|
||||
it("returns an empty object for an invalid JSON string", () => {
|
||||
// EJSON.parse returns undefined → Object(undefined) → {}
|
||||
expect(deserializeQuery("not-json")).toEqual({});
|
||||
});
|
||||
});
|
||||
Vendored
-1
@@ -1 +0,0 @@
|
||||
export {};
|
||||
Vendored
-52
@@ -1,52 +0,0 @@
|
||||
import { describe, it, expect } from "bun:test";
|
||||
import EJSON from "../../utils/ejson";
|
||||
describe("EJSON.parse", () => {
|
||||
it("parses a valid JSON string", () => {
|
||||
expect(EJSON.parse('{"a":1}')).toEqual({ a: 1 });
|
||||
});
|
||||
it("parses a JSON array string", () => {
|
||||
expect(EJSON.parse('[1,2,3]')).toEqual([1, 2, 3]);
|
||||
});
|
||||
it("returns undefined for null input", () => {
|
||||
expect(EJSON.parse(null)).toBeUndefined();
|
||||
});
|
||||
it("returns undefined for empty string", () => {
|
||||
expect(EJSON.parse("")).toBeUndefined();
|
||||
});
|
||||
it("returns undefined for invalid JSON", () => {
|
||||
expect(EJSON.parse("{bad json")).toBeUndefined();
|
||||
});
|
||||
it("returns the object directly when passed an object (typeof object)", () => {
|
||||
const obj = { x: 1 };
|
||||
expect(EJSON.parse(obj)).toBe(obj);
|
||||
});
|
||||
it("returns undefined for a number input", () => {
|
||||
expect(EJSON.parse(42)).toBeUndefined();
|
||||
});
|
||||
it("applies a reviver function", () => {
|
||||
const result = EJSON.parse('{"a":"2"}', (key, value) => key === "a" ? Number(value) : value);
|
||||
expect(result).toEqual({ a: 2 });
|
||||
});
|
||||
});
|
||||
describe("EJSON.stringify", () => {
|
||||
it("stringifies an object", () => {
|
||||
expect(EJSON.stringify({ a: 1 })).toBe('{"a":1}');
|
||||
});
|
||||
it("stringifies an array", () => {
|
||||
expect(EJSON.stringify([1, 2, 3])).toBe("[1,2,3]");
|
||||
});
|
||||
it("applies spacing", () => {
|
||||
expect(EJSON.stringify({ a: 1 }, null, 2)).toBe('{\n "a": 1\n}');
|
||||
});
|
||||
it("returns undefined for circular references", () => {
|
||||
const obj = {};
|
||||
obj.self = obj;
|
||||
expect(EJSON.stringify(obj)).toBeUndefined();
|
||||
});
|
||||
it("stringifies null", () => {
|
||||
expect(EJSON.stringify(null)).toBe("null");
|
||||
});
|
||||
it("stringifies a number", () => {
|
||||
expect(EJSON.stringify(42)).toBe("42");
|
||||
});
|
||||
});
|
||||
@@ -1 +0,0 @@
|
||||
export {};
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
import { describe, it, expect } from "bun:test";
|
||||
import AppNames from "../../utils/grab-app-names";
|
||||
describe("AppNames", () => {
|
||||
it("has a defaultPort of 7000", () => {
|
||||
expect(AppNames.defaultPort).toBe(7000);
|
||||
});
|
||||
it("has the correct defaultAssetPrefix", () => {
|
||||
expect(AppNames.defaultAssetPrefix).toBe("_bunext/static");
|
||||
});
|
||||
it("has name Bunext", () => {
|
||||
expect(AppNames.name).toBe("Bunext");
|
||||
});
|
||||
it("has a version string", () => {
|
||||
expect(typeof AppNames.version).toBe("string");
|
||||
expect(AppNames.version.length).toBeGreaterThan(0);
|
||||
});
|
||||
it("has defaultDistDir as .bunext", () => {
|
||||
expect(AppNames.defaultDistDir).toBe(".bunext");
|
||||
});
|
||||
it("has RootPagesComponentName as __root", () => {
|
||||
expect(AppNames.RootPagesComponentName).toBe("__root");
|
||||
});
|
||||
});
|
||||
@@ -1 +0,0 @@
|
||||
export {};
|
||||
-38
@@ -1,38 +0,0 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
|
||||
import grabAppPort from "../../utils/grab-app-port";
|
||||
const originalEnv = process.env.PORT;
|
||||
beforeEach(() => {
|
||||
delete process.env.PORT;
|
||||
global.CONFIG = {};
|
||||
});
|
||||
afterEach(() => {
|
||||
if (originalEnv !== undefined) {
|
||||
process.env.PORT = originalEnv;
|
||||
}
|
||||
else {
|
||||
delete process.env.PORT;
|
||||
}
|
||||
});
|
||||
describe("grabAppPort", () => {
|
||||
it("returns the default port (7000) when no config or env set", () => {
|
||||
expect(grabAppPort()).toBe(7000);
|
||||
});
|
||||
it("uses PORT env variable when set", () => {
|
||||
process.env.PORT = "8080";
|
||||
expect(grabAppPort()).toBe(8080);
|
||||
});
|
||||
it("uses config.port when PORT env is not set", () => {
|
||||
global.CONFIG = { port: 3000 };
|
||||
expect(grabAppPort()).toBe(3000);
|
||||
});
|
||||
it("PORT env takes precedence over config.port", () => {
|
||||
process.env.PORT = "9000";
|
||||
global.CONFIG = { port: 3000 };
|
||||
expect(grabAppPort()).toBe(9000);
|
||||
});
|
||||
it("handles non-numeric PORT env gracefully via numberfy", () => {
|
||||
process.env.PORT = "abc";
|
||||
// numberfy strips non-numeric chars, "abc" → "" → 0
|
||||
expect(grabAppPort()).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -1 +0,0 @@
|
||||
export {};
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
import { describe, it, expect, beforeEach } from "bun:test";
|
||||
import grabConstants from "../../utils/grab-constants";
|
||||
beforeEach(() => {
|
||||
global.CONFIG = {};
|
||||
});
|
||||
describe("grabConstants", () => {
|
||||
it("has the correct ClientRootElementIDName", () => {
|
||||
expect(grabConstants().ClientRootElementIDName).toBe("__bunext");
|
||||
});
|
||||
it("has the correct ClientWindowPagePropsName", () => {
|
||||
expect(grabConstants().ClientWindowPagePropsName).toBe("__PAGE_PROPS__");
|
||||
});
|
||||
it("has the correct ClientRootComponentWindowName", () => {
|
||||
expect(grabConstants().ClientRootComponentWindowName).toBe("BUNEXT_ROOT");
|
||||
});
|
||||
it("calculates MBInBytes as 1024 * 1024", () => {
|
||||
expect(grabConstants().MBInBytes).toBe(1024 * 1024);
|
||||
});
|
||||
it("ServerDefaultRequestBodyLimitBytes is 10 MB", () => {
|
||||
expect(grabConstants().ServerDefaultRequestBodyLimitBytes).toBe(10 * 1024 * 1024);
|
||||
});
|
||||
it("MaxBundlerRebuilds is 5", () => {
|
||||
expect(grabConstants().MaxBundlerRebuilds).toBe(5);
|
||||
});
|
||||
it("returns the current global.CONFIG", () => {
|
||||
const cfg = { port: 9000 };
|
||||
global.CONFIG = cfg;
|
||||
expect(grabConstants().config).toBe(cfg);
|
||||
});
|
||||
});
|
||||
@@ -1 +0,0 @@
|
||||
export {};
|
||||
-55
@@ -1,55 +0,0 @@
|
||||
import { describe, it, expect } from "bun:test";
|
||||
import path from "path";
|
||||
import grabDirNames from "../../utils/grab-dir-names";
|
||||
describe("grabDirNames", () => {
|
||||
it("derives all paths from process.cwd()", () => {
|
||||
const cwd = process.cwd();
|
||||
const dirs = grabDirNames();
|
||||
expect(dirs.ROOT_DIR).toBe(cwd);
|
||||
expect(dirs.SRC_DIR).toBe(path.join(cwd, "src"));
|
||||
expect(dirs.PAGES_DIR).toBe(path.join(cwd, "src", "pages"));
|
||||
expect(dirs.API_DIR).toBe(path.join(cwd, "src", "pages", "api"));
|
||||
expect(dirs.PUBLIC_DIR).toBe(path.join(cwd, "public"));
|
||||
});
|
||||
it("nests HYDRATION_DST_DIR under public/__bunext/pages", () => {
|
||||
const dirs = grabDirNames();
|
||||
expect(dirs.HYDRATION_DST_DIR).toBe(path.join(dirs.PUBLIC_DIR, "__bunext", "pages"));
|
||||
});
|
||||
it("nests BUNEXT_CACHE_DIR under public/__bunext/cache", () => {
|
||||
const dirs = grabDirNames();
|
||||
expect(dirs.BUNEXT_CACHE_DIR).toBe(path.join(dirs.PUBLIC_DIR, "__bunext", "cache"));
|
||||
});
|
||||
it("places map JSON file inside HYDRATION_DST_DIR", () => {
|
||||
const dirs = grabDirNames();
|
||||
expect(dirs.HYDRATION_DST_DIR_MAP_JSON_FILE).toBe(path.join(dirs.HYDRATION_DST_DIR, "map.json"));
|
||||
});
|
||||
it("places CONFIG_FILE at root", () => {
|
||||
const dirs = grabDirNames();
|
||||
expect(dirs.CONFIG_FILE).toBe(path.join(dirs.ROOT_DIR, "bunext.config.ts"));
|
||||
});
|
||||
it("places BUNX_TMP_DIR inside .bunext", () => {
|
||||
const dirs = grabDirNames();
|
||||
expect(dirs.BUNX_TMP_DIR).toContain(".bunext");
|
||||
expect(dirs.BUNX_TMP_DIR).toEndWith(".tmp");
|
||||
});
|
||||
it("places BUNX_HYDRATION_SRC_DIR under client/hydration-src", () => {
|
||||
const dirs = grabDirNames();
|
||||
expect(dirs.BUNX_HYDRATION_SRC_DIR).toContain(path.join("client", "hydration-src"));
|
||||
});
|
||||
it("sets 404 file name to not-found", () => {
|
||||
const dirs = grabDirNames();
|
||||
expect(dirs.BUNX_ROOT_404_FILE_NAME).toBe("not-found");
|
||||
});
|
||||
it("sets 500 file name to server-error", () => {
|
||||
const dirs = grabDirNames();
|
||||
expect(dirs.BUNX_ROOT_500_FILE_NAME).toBe("server-error");
|
||||
});
|
||||
it("preset 404 component path ends with not-found.tsx", () => {
|
||||
const dirs = grabDirNames();
|
||||
expect(dirs.BUNX_ROOT_404_PRESET_COMPONENT).toEndWith("not-found.tsx");
|
||||
});
|
||||
it("preset 500 component path ends with server-error.tsx", () => {
|
||||
const dirs = grabDirNames();
|
||||
expect(dirs.BUNX_ROOT_500_PRESET_COMPONENT).toEndWith("server-error.tsx");
|
||||
});
|
||||
});
|
||||
-1
@@ -1 +0,0 @@
|
||||
export {};
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
|
||||
import grabOrigin from "../../utils/grab-origin";
|
||||
const originalPort = process.env.PORT;
|
||||
beforeEach(() => {
|
||||
delete process.env.PORT;
|
||||
global.CONFIG = {};
|
||||
});
|
||||
afterEach(() => {
|
||||
if (originalPort !== undefined) {
|
||||
process.env.PORT = originalPort;
|
||||
}
|
||||
else {
|
||||
delete process.env.PORT;
|
||||
}
|
||||
});
|
||||
describe("grabOrigin", () => {
|
||||
it("returns config.origin when set", () => {
|
||||
global.CONFIG = { origin: "https://example.com" };
|
||||
expect(grabOrigin()).toBe("https://example.com");
|
||||
});
|
||||
it("falls back to http://localhost:<port> using default port", () => {
|
||||
expect(grabOrigin()).toBe("http://localhost:7000");
|
||||
});
|
||||
it("falls back using PORT env variable", () => {
|
||||
process.env.PORT = "8080";
|
||||
expect(grabOrigin()).toBe("http://localhost:8080");
|
||||
});
|
||||
it("falls back using config.port", () => {
|
||||
global.CONFIG = { port: 3700 };
|
||||
expect(grabOrigin()).toBe("http://localhost:3700");
|
||||
});
|
||||
});
|
||||
@@ -1 +0,0 @@
|
||||
export {};
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
import { describe, it, expect } from "bun:test";
|
||||
import grabPageName from "../../utils/grab-page-name";
|
||||
describe("grabPageName", () => {
|
||||
it("returns the page name for a simple page path", () => {
|
||||
expect(grabPageName({ path: "/home/user/project/src/pages/about.tsx" }))
|
||||
.toBe("about");
|
||||
});
|
||||
it("returns 'index' for a root index file (no -index stripping at root)", () => {
|
||||
// -index suffix is only stripped when joined: e.g. "blog-index" → "blog"
|
||||
// A standalone "index" filename has no leading dash so stays as-is
|
||||
expect(grabPageName({ path: "/home/user/project/src/pages/index.tsx" }))
|
||||
.toBe("index");
|
||||
});
|
||||
it("handles nested page paths", () => {
|
||||
expect(grabPageName({ path: "/home/user/project/src/pages/blog/post.tsx" })).toBe("blog-post");
|
||||
});
|
||||
it("strips -index suffix from nested index files", () => {
|
||||
expect(grabPageName({ path: "/home/user/project/src/pages/blog/index.tsx" })).toBe("blog");
|
||||
});
|
||||
it("converts dynamic segments [slug] by replacing brackets", () => {
|
||||
const result = grabPageName({
|
||||
path: "/home/user/project/src/pages/blog/[slug].tsx",
|
||||
});
|
||||
// [ → - and ] is dropped (not a-z or -), so [slug] → -slug
|
||||
expect(result).toBe("blog--slug");
|
||||
});
|
||||
it("converts spread [...params] segments", () => {
|
||||
const result = grabPageName({
|
||||
path: "/home/user/project/src/pages/[...params].tsx",
|
||||
});
|
||||
// "[...params]" → remove ext → "[...params]"
|
||||
// [ → "-" → "-...params]"
|
||||
// "..." → "-" → "--params]"
|
||||
// strip non [a-z-] → "--params"
|
||||
expect(result).toBe("--params");
|
||||
});
|
||||
it("strips uppercase letters (only a-z and - are kept)", () => {
|
||||
// [^a-z\-] strips uppercase — 'A' is removed, 'bout' remains
|
||||
expect(grabPageName({ path: "/home/user/project/src/pages/About.tsx" })).toBe("bout");
|
||||
});
|
||||
it("handles deeply nested paths", () => {
|
||||
expect(grabPageName({
|
||||
path: "/home/user/project/src/pages/admin/users/list.tsx",
|
||||
})).toBe("admin-users-list");
|
||||
});
|
||||
});
|
||||
@@ -1 +0,0 @@
|
||||
export {};
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
|
||||
import isDevelopment from "../../utils/is-development";
|
||||
const originalEnv = process.env.NODE_ENV;
|
||||
beforeEach(() => {
|
||||
// Reset global config before each test
|
||||
global.CONFIG = {};
|
||||
});
|
||||
afterEach(() => {
|
||||
process.env.NODE_ENV = originalEnv;
|
||||
});
|
||||
describe("isDevelopment", () => {
|
||||
it("returns false when NODE_ENV is production", () => {
|
||||
process.env.NODE_ENV = "production";
|
||||
global.CONFIG = { development: true };
|
||||
expect(isDevelopment()).toBe(false);
|
||||
});
|
||||
it("returns true when config.development is true and NODE_ENV is not production", () => {
|
||||
process.env.NODE_ENV = "development";
|
||||
global.CONFIG = { development: true };
|
||||
expect(isDevelopment()).toBe(true);
|
||||
});
|
||||
it("returns false when config.development is false", () => {
|
||||
process.env.NODE_ENV = "development";
|
||||
global.CONFIG = { development: false };
|
||||
expect(isDevelopment()).toBe(false);
|
||||
});
|
||||
it("returns false when config.development is undefined", () => {
|
||||
process.env.NODE_ENV = "development";
|
||||
global.CONFIG = {};
|
||||
expect(isDevelopment()).toBe(false);
|
||||
});
|
||||
});
|
||||
-1
@@ -1 +0,0 @@
|
||||
export {};
|
||||
-43
@@ -1,43 +0,0 @@
|
||||
import { describe, it, expect } from "bun:test";
|
||||
import numberfy, { _n } from "../../utils/numberfy";
|
||||
describe("numberfy", () => {
|
||||
it("converts a plain integer string", () => {
|
||||
expect(numberfy("42")).toBe(42);
|
||||
});
|
||||
it("converts a float string preserving decimals", () => {
|
||||
expect(numberfy("3.14")).toBe(3.14);
|
||||
});
|
||||
it("strips non-numeric characters", () => {
|
||||
expect(numberfy("$1,234.56")).toBe(1234.56);
|
||||
});
|
||||
it("returns 0 for an empty string", () => {
|
||||
expect(numberfy("")).toBe(0);
|
||||
});
|
||||
it("returns 0 for undefined", () => {
|
||||
expect(numberfy(undefined)).toBe(0);
|
||||
});
|
||||
it("returns 0 for null", () => {
|
||||
expect(numberfy(null)).toBe(0);
|
||||
});
|
||||
it("passes through a number directly", () => {
|
||||
expect(numberfy(7)).toBe(7);
|
||||
});
|
||||
it("rounds when no decimals specified and no decimal in input", () => {
|
||||
expect(numberfy("5.0")).toBe(5);
|
||||
});
|
||||
it("respects decimals=0 by rounding", () => {
|
||||
expect(numberfy("3.7", 0)).toBe(4);
|
||||
});
|
||||
it("respects explicit decimals parameter", () => {
|
||||
expect(numberfy("3.14159", 2)).toBe(3.14);
|
||||
});
|
||||
it("preserves existing decimal places when no decimals arg given", () => {
|
||||
expect(numberfy("1.500")).toBe(1.5);
|
||||
});
|
||||
it("strips a trailing dot", () => {
|
||||
expect(numberfy("5.")).toBe(5);
|
||||
});
|
||||
it("_n alias works identically", () => {
|
||||
expect(_n("10")).toBe(10);
|
||||
});
|
||||
});
|
||||
Vendored
-2
@@ -1,2 +0,0 @@
|
||||
#!/usr/bin/env bun
|
||||
export {};
|
||||
Vendored
-131
@@ -1,131 +0,0 @@
|
||||
#!/usr/bin/env bun
|
||||
import plugin from "bun-plugin-tailwind";
|
||||
import { existsSync } from "fs";
|
||||
import { rm } from "fs/promises";
|
||||
import path from "path";
|
||||
if (process.argv.includes("--help") || process.argv.includes("-h")) {
|
||||
console.log(`
|
||||
🏗️ Bun Build Script
|
||||
|
||||
Usage: bun run build.ts [options]
|
||||
|
||||
Common Options:
|
||||
--outdir <path> Output directory (default: "dist")
|
||||
--minify Enable minification (or --minify.whitespace, --minify.syntax, etc)
|
||||
--sourcemap <type> Sourcemap type: none|linked|inline|external
|
||||
--target <target> Build target: browser|bun|node
|
||||
--format <format> Output format: esm|cjs|iife
|
||||
--splitting Enable code splitting
|
||||
--packages <type> Package handling: bundle|external
|
||||
--public-path <path> Public path for assets
|
||||
--env <mode> Environment handling: inline|disable|prefix*
|
||||
--conditions <list> Package.json export conditions (comma separated)
|
||||
--external <list> External packages (comma separated)
|
||||
--banner <text> Add banner text to output
|
||||
--footer <text> Add footer text to output
|
||||
--define <obj> Define global constants (e.g. --define.VERSION=1.0.0)
|
||||
--help, -h Show this help message
|
||||
|
||||
Example:
|
||||
bun run build.ts --outdir=dist --minify --sourcemap=linked --external=react,react-dom
|
||||
`);
|
||||
process.exit(0);
|
||||
}
|
||||
const toCamelCase = (str) => str.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
|
||||
const parseValue = (value) => {
|
||||
if (value === "true")
|
||||
return true;
|
||||
if (value === "false")
|
||||
return false;
|
||||
if (/^\d+$/.test(value))
|
||||
return parseInt(value, 10);
|
||||
if (/^\d*\.\d+$/.test(value))
|
||||
return parseFloat(value);
|
||||
if (value.includes(","))
|
||||
return value.split(",").map((v) => v.trim());
|
||||
return value;
|
||||
};
|
||||
function parseArgs() {
|
||||
const config = {};
|
||||
const args = process.argv.slice(2);
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
if (arg === undefined)
|
||||
continue;
|
||||
if (!arg.startsWith("--"))
|
||||
continue;
|
||||
if (arg.startsWith("--no-")) {
|
||||
const key = toCamelCase(arg.slice(5));
|
||||
config[key] = false;
|
||||
continue;
|
||||
}
|
||||
if (!arg.includes("=") &&
|
||||
(i === args.length - 1 || args[i + 1]?.startsWith("--"))) {
|
||||
const key = toCamelCase(arg.slice(2));
|
||||
config[key] = true;
|
||||
continue;
|
||||
}
|
||||
let key;
|
||||
let value;
|
||||
if (arg.includes("=")) {
|
||||
[key, value] = arg.slice(2).split("=", 2);
|
||||
}
|
||||
else {
|
||||
key = arg.slice(2);
|
||||
value = args[++i] ?? "";
|
||||
}
|
||||
key = toCamelCase(key);
|
||||
if (key.includes(".")) {
|
||||
const [parentKey, childKey] = key.split(".");
|
||||
config[parentKey] = config[parentKey] || {};
|
||||
config[parentKey][childKey] = parseValue(value);
|
||||
}
|
||||
else {
|
||||
config[key] = parseValue(value);
|
||||
}
|
||||
}
|
||||
return config;
|
||||
}
|
||||
const formatFileSize = (bytes) => {
|
||||
const units = ["B", "KB", "MB", "GB"];
|
||||
let size = bytes;
|
||||
let unitIndex = 0;
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
return `${size.toFixed(2)} ${units[unitIndex]}`;
|
||||
};
|
||||
console.log("\n🚀 Starting build process...\n");
|
||||
const cliConfig = parseArgs();
|
||||
const outdir = cliConfig.outdir || path.join(process.cwd(), "dist");
|
||||
if (existsSync(outdir)) {
|
||||
console.log(`🗑️ Cleaning previous build at ${outdir}`);
|
||||
await rm(outdir, { recursive: true, force: true });
|
||||
}
|
||||
const start = performance.now();
|
||||
const entrypoints = [...new Bun.Glob("**.html").scanSync("src/app")]
|
||||
.map((a) => path.resolve("src/app", a))
|
||||
.filter((dir) => !dir.includes("node_modules"));
|
||||
console.log(`📄 Found ${entrypoints.length} HTML ${entrypoints.length === 1 ? "file" : "files"} to process\n`);
|
||||
const result = await Bun.build({
|
||||
entrypoints,
|
||||
outdir,
|
||||
plugins: [plugin],
|
||||
minify: true,
|
||||
target: "browser",
|
||||
sourcemap: "linked",
|
||||
define: {
|
||||
"process.env.NODE_ENV": JSON.stringify("production"),
|
||||
},
|
||||
...cliConfig,
|
||||
});
|
||||
const end = performance.now();
|
||||
const outputTable = result.outputs.map((output) => ({
|
||||
File: path.relative(process.cwd(), output.path),
|
||||
Type: output.kind,
|
||||
Size: formatFileSize(output.size),
|
||||
}));
|
||||
console.table(outputTable);
|
||||
const buildTime = (end - start).toFixed(2);
|
||||
console.log(`\n✅ Build completed in ${buildTime}ms\n`);
|
||||
Vendored
+13
-10
@@ -1,18 +1,21 @@
|
||||
import { Command } from "commander";
|
||||
import allPagesBundler from "../../functions/bundler/all-pages-bundler";
|
||||
import { log } from "../../utils/log";
|
||||
import init from "../../functions/init";
|
||||
import grabDirNames from "../../utils/grab-dir-names";
|
||||
import { rmSync } from "fs";
|
||||
import bunextInit from "../../functions/bunext-init";
|
||||
const { HYDRATION_DST_DIR, BUNX_CWD_PAGES_REWRITE_DIR } = grabDirNames();
|
||||
export default function () {
|
||||
return new Command("build")
|
||||
.description("Build Project")
|
||||
.action(async () => {
|
||||
process.env.NODE_ENV = "production";
|
||||
process.env.BUILD = "true";
|
||||
await init();
|
||||
log.banner();
|
||||
log.build("Building Project ...");
|
||||
allPagesBundler({
|
||||
exit_after_first_build: true,
|
||||
});
|
||||
try {
|
||||
rmSync(HYDRATION_DST_DIR, { recursive: true });
|
||||
rmSync(BUNX_CWD_PAGES_REWRITE_DIR, { recursive: true });
|
||||
}
|
||||
catch (error) { }
|
||||
global.BUNEXT_SKIPPED_BROWSER_MODULES = new Set();
|
||||
await bunextInit({ build_only: true });
|
||||
log.success("Modules Built Successfully!");
|
||||
process.exit();
|
||||
});
|
||||
}
|
||||
|
||||
Vendored
Vendored
+25
@@ -0,0 +1,25 @@
|
||||
import startServer from "../../functions/server/start-server";
|
||||
import { log } from "../../utils/log";
|
||||
import bunextInit from "../../functions/bunext-init";
|
||||
import grabDirNames from "../../utils/grab-dir-names";
|
||||
import { rmSync } from "fs";
|
||||
const { HYDRATION_DST_DIR, BUNX_CWD_PAGES_REWRITE_DIR } = grabDirNames();
|
||||
process.on("uncaughtException", (error) => {
|
||||
log.error(`Uncaught exception: ${error}`);
|
||||
});
|
||||
process.on("unhandledRejection", (reason) => {
|
||||
log.error(`Unhandled rejection: ${reason}`);
|
||||
});
|
||||
log.info("Running development server ...");
|
||||
try {
|
||||
rmSync(HYDRATION_DST_DIR, { recursive: true });
|
||||
rmSync(BUNX_CWD_PAGES_REWRITE_DIR, { recursive: true });
|
||||
}
|
||||
catch (error) { }
|
||||
try {
|
||||
await bunextInit();
|
||||
await startServer();
|
||||
}
|
||||
catch (error) {
|
||||
log.error(`Failed to start development server: ${error}`);
|
||||
}
|
||||
Vendored
+54
-7
@@ -1,14 +1,61 @@
|
||||
import { Command } from "commander";
|
||||
import startServer from "../../functions/server/start-server";
|
||||
import { log } from "../../utils/log";
|
||||
import bunextInit from "../../functions/bunext-init";
|
||||
import path from "path";
|
||||
import grabDirNames from "../../utils/grab-dir-names";
|
||||
import writeErrorFile from "../../functions/write-error-file";
|
||||
import { existsSync } from "fs";
|
||||
let retries = 0;
|
||||
let timeout;
|
||||
const MAX_RETRIES = 5;
|
||||
export default function () {
|
||||
return new Command("dev")
|
||||
.description("Run development server")
|
||||
.action(async () => {
|
||||
process.env.NODE_ENV == "development";
|
||||
log.info("Running development server ...");
|
||||
await bunextInit();
|
||||
await startServer();
|
||||
await dev();
|
||||
});
|
||||
}
|
||||
async function dev() {
|
||||
clearTimeout(timeout);
|
||||
if (retries >= MAX_RETRIES) {
|
||||
console.error(`Dev server crashed ${MAX_RETRIES} times. Exiting.`);
|
||||
process.exit(1);
|
||||
}
|
||||
const dev_spawn_file = path.resolve(__dirname, "dev-spawn.ts");
|
||||
const dev_spawn_js_file = path.resolve(__dirname, "dev-spawn.js");
|
||||
const final_spawn_file = existsSync(dev_spawn_js_file)
|
||||
? dev_spawn_js_file
|
||||
: dev_spawn_file;
|
||||
const spawn_options = {
|
||||
cmd: ["bun", final_spawn_file],
|
||||
stdio: ["inherit", "inherit", "inherit"],
|
||||
async onExit(subprocess, exitCode, signalCode, error) {
|
||||
writeErrorFile({ exitCode, error });
|
||||
},
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_ENV: "development",
|
||||
},
|
||||
};
|
||||
let dev_process;
|
||||
try {
|
||||
dev_process = Bun.spawn(spawn_options);
|
||||
}
|
||||
catch (error) {
|
||||
console.error(`Failed to start dev process:`, error);
|
||||
retries++;
|
||||
timeout = setTimeout(() => {
|
||||
retries = 0;
|
||||
}, 10000);
|
||||
return await dev();
|
||||
}
|
||||
const exited = await dev_process.exited;
|
||||
if (exited) {
|
||||
retries++;
|
||||
timeout = setTimeout(() => {
|
||||
retries = 0;
|
||||
}, 10000);
|
||||
return await dev();
|
||||
}
|
||||
timeout = setTimeout(() => {
|
||||
retries = 0;
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
Vendored
+5
-2
@@ -3,13 +3,14 @@ import { program } from "commander";
|
||||
import start from "./start";
|
||||
import dev from "./dev";
|
||||
import build from "./build";
|
||||
import { log } from "../utils/log";
|
||||
/**
|
||||
* # Describe Program
|
||||
*/
|
||||
program
|
||||
.name(`bunext`)
|
||||
.description(`A React Next JS replacement built with bun JS`)
|
||||
.version(`1.0.0`);
|
||||
.version(`1.0.43`);
|
||||
/**
|
||||
* # Declare Commands
|
||||
*/
|
||||
@@ -20,7 +21,9 @@ program.addCommand(build());
|
||||
* # Handle Unavailable Commands
|
||||
*/
|
||||
program.on("command:*", () => {
|
||||
console.error("Invalid command: %s\nSee --help for a list of available commands.", program.args.join(" "));
|
||||
log.error("Invalid command: %s\nSee --help for a list of available commands." +
|
||||
" " +
|
||||
program.args.join(" "));
|
||||
process.exit(1);
|
||||
});
|
||||
/**
|
||||
|
||||
Vendored
+53
-7
@@ -1,14 +1,60 @@
|
||||
import { Command } from "commander";
|
||||
import startServer from "../../functions/server/start-server";
|
||||
import { log } from "../../utils/log";
|
||||
import bunextInit from "../../functions/bunext-init";
|
||||
import path from "path";
|
||||
import writeErrorFile from "../../functions/write-error-file";
|
||||
import { existsSync } from "fs";
|
||||
let retries = 0;
|
||||
let timeout;
|
||||
const MAX_RETRIES = 5;
|
||||
export default function () {
|
||||
return new Command("start")
|
||||
.description("Start production server")
|
||||
.action(async () => {
|
||||
process.env.NODE_ENV = "production";
|
||||
log.info("Starting production server ...");
|
||||
await bunextInit();
|
||||
await startServer();
|
||||
await start();
|
||||
});
|
||||
}
|
||||
async function start() {
|
||||
clearTimeout(timeout);
|
||||
if (retries >= MAX_RETRIES) {
|
||||
console.error(`Production server crashed ${MAX_RETRIES} times. Exiting.`);
|
||||
process.exit(1);
|
||||
}
|
||||
const prod_spawn_file = path.resolve(__dirname, "prod-spawn.ts");
|
||||
const prod_spawn_js_file = path.resolve(__dirname, "prod-spawn.js");
|
||||
const final_spawn_file = existsSync(prod_spawn_js_file)
|
||||
? prod_spawn_js_file
|
||||
: prod_spawn_file;
|
||||
const spawn_options = {
|
||||
cmd: ["bun", final_spawn_file],
|
||||
stdio: ["inherit", "inherit", "inherit"],
|
||||
onExit(subprocess, exitCode, signalCode, error) {
|
||||
writeErrorFile({ exitCode, error });
|
||||
},
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_ENV: "production",
|
||||
},
|
||||
};
|
||||
let dev_process;
|
||||
try {
|
||||
dev_process = Bun.spawn(spawn_options);
|
||||
}
|
||||
catch (error) {
|
||||
console.error(`Failed to start production process:`, error);
|
||||
retries++;
|
||||
timeout = setTimeout(() => {
|
||||
retries = 0;
|
||||
}, 10000);
|
||||
return await start();
|
||||
}
|
||||
const exited = await dev_process.exited;
|
||||
if (exited) {
|
||||
retries++;
|
||||
timeout = setTimeout(() => {
|
||||
retries = 0;
|
||||
}, 10000);
|
||||
return await start();
|
||||
}
|
||||
timeout = setTimeout(() => {
|
||||
retries = 0;
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
Vendored
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
import bunextInit from "../../functions/bunext-init";
|
||||
import startServer from "../../functions/server/start-server";
|
||||
import { log } from "../../utils/log";
|
||||
log.info("Starting production server ...");
|
||||
await bunextInit();
|
||||
await startServer();
|
||||
Vendored
+3
@@ -4,4 +4,7 @@ export declare const AppData: {
|
||||
readonly BunextStaticFilesCacheExpiry: number;
|
||||
readonly ClientHMRPath: "__bunext_client_hmr__";
|
||||
readonly BunextClientHydrationScriptID: "bunext-client-hydration-script";
|
||||
readonly BunextTmpFileExt: ".bunext_tmp.tsx";
|
||||
readonly BunextHMRRetryRoute: "/.bunext/hmr-retry";
|
||||
readonly DefaultMaxLogs: 50;
|
||||
};
|
||||
|
||||
Vendored
+3
@@ -4,4 +4,7 @@ export const AppData = {
|
||||
BunextStaticFilesCacheExpiry: 60 * 60 * 24 * 7,
|
||||
ClientHMRPath: "__bunext_client_hmr__",
|
||||
BunextClientHydrationScriptID: "bunext-client-hydration-script",
|
||||
BunextTmpFileExt: ".bunext_tmp.tsx",
|
||||
BunextHMRRetryRoute: "/.bunext/hmr-retry",
|
||||
DefaultMaxLogs: 50,
|
||||
};
|
||||
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
import type { BundlerCTXMap } from "../../types";
|
||||
type Params = {
|
||||
watch?: boolean;
|
||||
exit_after_first_build?: boolean;
|
||||
post_build_fn?: (params: {
|
||||
artifacts: BundlerCTXMap[];
|
||||
}) => Promise<void>;
|
||||
};
|
||||
export default function allPagesBundler(params?: Params): Promise<void>;
|
||||
export {};
|
||||
-90
@@ -1,90 +0,0 @@
|
||||
import { writeFileSync } from "fs";
|
||||
import * as esbuild from "esbuild";
|
||||
import grabAllPages from "../../utils/grab-all-pages";
|
||||
import grabDirNames from "../../utils/grab-dir-names";
|
||||
import isDevelopment from "../../utils/is-development";
|
||||
import { execSync } from "child_process";
|
||||
import { log } from "../../utils/log";
|
||||
import tailwindEsbuildPlugin from "../server/web-pages/tailwind-esbuild-plugin";
|
||||
import grabClientHydrationScript from "./grab-client-hydration-script";
|
||||
import grabArtifactsFromBundledResults from "./grab-artifacts-from-bundled-result";
|
||||
const { HYDRATION_DST_DIR, HYDRATION_DST_DIR_MAP_JSON_FILE } = grabDirNames();
|
||||
export default async function allPagesBundler(params) {
|
||||
const pages = grabAllPages({ exclude_api: true });
|
||||
const virtualEntries = {};
|
||||
const dev = isDevelopment();
|
||||
for (const page of pages) {
|
||||
const key = page.local_path;
|
||||
const txt = grabClientHydrationScript({
|
||||
page_local_path: page.local_path,
|
||||
});
|
||||
virtualEntries[key] = txt;
|
||||
}
|
||||
const virtualPlugin = {
|
||||
name: "virtual-entrypoints",
|
||||
setup(build) {
|
||||
build.onResolve({ filter: /^virtual:/ }, (args) => ({
|
||||
path: args.path.replace("virtual:", ""),
|
||||
namespace: "virtual",
|
||||
}));
|
||||
build.onLoad({ filter: /.*/, namespace: "virtual" }, (args) => ({
|
||||
contents: virtualEntries[args.path],
|
||||
loader: "tsx",
|
||||
resolveDir: process.cwd(),
|
||||
}));
|
||||
},
|
||||
};
|
||||
const artifactTracker = {
|
||||
name: "artifact-tracker",
|
||||
setup(build) {
|
||||
let buildStart = 0;
|
||||
build.onStart(() => {
|
||||
buildStart = performance.now();
|
||||
});
|
||||
build.onEnd((result) => {
|
||||
if (result.errors.length > 0)
|
||||
return;
|
||||
const artifacts = grabArtifactsFromBundledResults({
|
||||
pages,
|
||||
result,
|
||||
});
|
||||
if (artifacts?.[0] && artifacts.length > 0) {
|
||||
global.BUNDLER_CTX_MAP = artifacts;
|
||||
global.PAGE_FILES = pages;
|
||||
params?.post_build_fn?.({ artifacts });
|
||||
writeFileSync(HYDRATION_DST_DIR_MAP_JSON_FILE, JSON.stringify(artifacts));
|
||||
}
|
||||
const elapsed = (performance.now() - buildStart).toFixed(0);
|
||||
log.success(`[Built] in ${elapsed}ms`);
|
||||
global.RECOMPILING = false;
|
||||
if (params?.exit_after_first_build) {
|
||||
process.exit();
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
execSync(`rm -rf ${HYDRATION_DST_DIR}`);
|
||||
const ctx = await esbuild.context({
|
||||
entryPoints: Object.keys(virtualEntries).map((k) => `virtual:${k}`),
|
||||
outdir: HYDRATION_DST_DIR,
|
||||
bundle: true,
|
||||
minify: true,
|
||||
format: "esm",
|
||||
target: "es2020",
|
||||
platform: "browser",
|
||||
define: {
|
||||
"process.env.NODE_ENV": JSON.stringify(dev ? "development" : "production"),
|
||||
},
|
||||
entryNames: "[dir]/[name]/[hash]",
|
||||
metafile: true,
|
||||
plugins: [tailwindEsbuildPlugin, virtualPlugin, artifactTracker],
|
||||
jsx: "automatic",
|
||||
splitting: true,
|
||||
logLevel: "silent",
|
||||
});
|
||||
await ctx.rebuild();
|
||||
if (params?.watch) {
|
||||
global.BUNDLER_CTX = ctx;
|
||||
// global.BUNDLER_CTX.watch();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { BundlerCTXMap } from "../../types";
|
||||
type Params = {
|
||||
post_build_fn?: (params: {
|
||||
artifacts: BundlerCTXMap[];
|
||||
}) => Promise<void> | void;
|
||||
build_only?: boolean;
|
||||
start?: boolean;
|
||||
};
|
||||
export default function allPagesESBuildContextBundler(params?: Params): Promise<void>;
|
||||
export {};
|
||||
@@ -0,0 +1,91 @@
|
||||
import * as esbuild from "esbuild";
|
||||
import grabAllPages from "../../utils/grab-all-pages";
|
||||
import grabDirNames from "../../utils/grab-dir-names";
|
||||
import isDevelopment from "../../utils/is-development";
|
||||
import tailwindEsbuildPlugin from "../server/web-pages/tailwind-esbuild-plugin";
|
||||
import grabClientHydrationScript from "./grab-client-hydration-script";
|
||||
import path from "path";
|
||||
import virtualFilesPlugin from "./plugins/virtual-files-plugin";
|
||||
import esbuildCTXArtifactTracker from "./plugins/esbuild-ctx-artifact-tracker";
|
||||
import { existsSync } from "fs";
|
||||
const { HYDRATION_DST_DIR, BUNX_HYDRATION_SRC_DIR, BUNX_BUNDLER_ERROR_EXIT_FILE, } = grabDirNames();
|
||||
export default async function allPagesESBuildContextBundler(params) {
|
||||
try {
|
||||
const did_process_exit_because_of_bundler_error = existsSync(BUNX_BUNDLER_ERROR_EXIT_FILE);
|
||||
const pages = grabAllPages({ exclude_api: true });
|
||||
global.BUNEXT_PAGE_FILES = pages;
|
||||
const dev = isDevelopment();
|
||||
const entryToPage = new Map();
|
||||
for (const page of pages) {
|
||||
const tsx = await grabClientHydrationScript({
|
||||
page_local_path: page.local_path,
|
||||
});
|
||||
if (!tsx) {
|
||||
continue;
|
||||
}
|
||||
const entryFile = path.join(BUNX_HYDRATION_SRC_DIR, `${page.url_path}.tsx`);
|
||||
// await Bun.write(entryFile, txt, { createPath: true });
|
||||
entryToPage.set(entryFile, { ...page, tsx });
|
||||
}
|
||||
const entryPoints = [...entryToPage.keys()].map((e) => `hydration-virtual:${e}`);
|
||||
global.BUNEXT_BUNDLER_CTX = await esbuild.context({
|
||||
entryPoints,
|
||||
outdir: HYDRATION_DST_DIR,
|
||||
bundle: true,
|
||||
minify: !dev,
|
||||
format: "esm",
|
||||
target: "es2020",
|
||||
platform: "browser",
|
||||
define: {
|
||||
"process.env.NODE_ENV": JSON.stringify(dev ? "development" : "production"),
|
||||
},
|
||||
entryNames: "[dir]/[hash]",
|
||||
metafile: true,
|
||||
plugins: [
|
||||
forceExternalReact(),
|
||||
tailwindEsbuildPlugin,
|
||||
virtualFilesPlugin({
|
||||
entryToPage,
|
||||
}),
|
||||
esbuildCTXArtifactTracker({
|
||||
entryToPage,
|
||||
post_build_fn: params?.post_build_fn,
|
||||
build_only: params?.build_only || params?.start,
|
||||
}),
|
||||
],
|
||||
jsx: "automatic",
|
||||
splitting: true,
|
||||
treeShaking: true,
|
||||
external: [
|
||||
"react",
|
||||
"react-dom",
|
||||
"react-dom/client",
|
||||
"react/jsx-runtime",
|
||||
"react/jsx-dev-runtime",
|
||||
...(global.BUNEXT_CONFIG.page_compiler_excludes || []),
|
||||
],
|
||||
logLevel: did_process_exit_because_of_bundler_error
|
||||
? "silent"
|
||||
: undefined,
|
||||
});
|
||||
await global.BUNEXT_BUNDLER_CTX.rebuild();
|
||||
}
|
||||
catch (error) {
|
||||
console.log(`ESBUILD Error =>`, error);
|
||||
}
|
||||
}
|
||||
function forceExternalReact() {
|
||||
return {
|
||||
name: "force-external-react",
|
||||
setup(build) {
|
||||
build.onResolve({ filter: /^react(-dom)?(\/.*)?$/ }, (args) => {
|
||||
if (args.pluginData?.externalReact)
|
||||
return null;
|
||||
return {
|
||||
path: args.path,
|
||||
external: true,
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export default function apiRoutesBundler(): Promise<void>;
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
import grabAllPages from "../../utils/grab-all-pages";
|
||||
import grabDirNames from "../../utils/grab-dir-names";
|
||||
import isDevelopment from "../../utils/is-development";
|
||||
import tailwindcss from "bun-plugin-tailwind";
|
||||
const { BUNX_CWD_MODULE_CACHE_DIR } = grabDirNames();
|
||||
export default async function apiRoutesBundler() {
|
||||
const api_routes = grabAllPages({ api_only: true });
|
||||
const dev = isDevelopment();
|
||||
try {
|
||||
const build = await Bun.build({
|
||||
entrypoints: api_routes.map((r) => r.local_path),
|
||||
target: "bun",
|
||||
format: "esm",
|
||||
jsx: {
|
||||
runtime: "automatic",
|
||||
development: dev,
|
||||
},
|
||||
minify: !dev,
|
||||
define: {
|
||||
"process.env.NODE_ENV": JSON.stringify(dev ? "development" : "production"),
|
||||
},
|
||||
outdir: BUNX_CWD_MODULE_CACHE_DIR,
|
||||
plugins: [tailwindcss],
|
||||
naming: {
|
||||
entry: "api/[dir]/[name].[ext]",
|
||||
chunk: "api/[dir]/chunks/[hash].[ext]",
|
||||
},
|
||||
// external: [
|
||||
// "react",
|
||||
// "react-dom",
|
||||
// "react-dom/client",
|
||||
// "react/jsx-runtime",
|
||||
// ],
|
||||
splitting: true,
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
console.log(`API paths build ERROR:`, error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export default function apiRoutesContextBundler(): Promise<void>;
|
||||
@@ -0,0 +1,44 @@
|
||||
import * as esbuild from "esbuild";
|
||||
import grabAllPages from "../../utils/grab-all-pages";
|
||||
import grabDirNames from "../../utils/grab-dir-names";
|
||||
import isDevelopment from "../../utils/is-development";
|
||||
import tailwindEsbuildPlugin from "../server/web-pages/tailwind-esbuild-plugin";
|
||||
import apiRoutesCTXArtifactTracker from "./plugins/api-routes-ctx-artifact-tracker";
|
||||
const { BUNX_CWD_MODULE_CACHE_DIR } = grabDirNames();
|
||||
export default async function apiRoutesContextBundler() {
|
||||
const pages = grabAllPages({ api_only: true });
|
||||
const dev = isDevelopment();
|
||||
// if (global.API_ROUTES_BUNDLER_CTX) {
|
||||
// await global.API_ROUTES_BUNDLER_CTX.dispose();
|
||||
// global.API_ROUTES_BUNDLER_CTX = undefined;
|
||||
// }
|
||||
// global.API_ROUTES_BUNDLER_CTX = await esbuild.context({
|
||||
// entryPoints: pages.map((p) => p.local_path),
|
||||
// outdir: BUNX_CWD_MODULE_CACHE_DIR,
|
||||
// bundle: true,
|
||||
// minify: !dev,
|
||||
// format: "esm",
|
||||
// target: "esnext",
|
||||
// platform: "node",
|
||||
// define: {
|
||||
// "process.env.NODE_ENV": JSON.stringify(
|
||||
// dev ? "development" : "production",
|
||||
// ),
|
||||
// },
|
||||
// entryNames: "api/[dir]/[hash]",
|
||||
// metafile: true,
|
||||
// plugins: [
|
||||
// tailwindEsbuildPlugin,
|
||||
// apiRoutesCTXArtifactTracker({ pages }),
|
||||
// ],
|
||||
// jsx: "automatic",
|
||||
// external: [
|
||||
// "react",
|
||||
// "react-dom",
|
||||
// "react/jsx-runtime",
|
||||
// "react/jsx-dev-runtime",
|
||||
// "bun:*",
|
||||
// ],
|
||||
// });
|
||||
// await global.API_ROUTES_BUNDLER_CTX.rebuild();
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
type Params = {};
|
||||
export default function buildOnstartErrorHandler(params?: Params): Promise<void>;
|
||||
export {};
|
||||
@@ -0,0 +1,18 @@
|
||||
export default async function buildOnstartErrorHandler(params) {
|
||||
// const error_msg = `Build Failed. Please check all your components and imports.`;
|
||||
// log.error(error_msg);
|
||||
if (global.BUNEXT_BUNDLER_CTX_DISPOSED) {
|
||||
return;
|
||||
}
|
||||
// console.log(`Killing Bundler ...`);
|
||||
// console.log(`global.BUNEXT_BUNDLER_CTX_DISPOSED`, global.BUNEXT_BUNDLER_CTX_DISPOSED);
|
||||
global.BUNEXT_BUNDLER_CTX_DISPOSED = true;
|
||||
global.BUNEXT_RECOMPILING = false;
|
||||
global.BUNEXT_IS_SERVER_COMPONENT = false;
|
||||
await Promise.all([
|
||||
global.BUNEXT_SSR_BUNDLER_CTX?.dispose(),
|
||||
global.BUNEXT_BUNDLER_CTX?.dispose(),
|
||||
]);
|
||||
global.BUNEXT_SSR_BUNDLER_CTX = undefined;
|
||||
global.BUNEXT_BUNDLER_CTX = undefined;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export default function bunReactModulesBundler(): Promise<void>;
|
||||
@@ -0,0 +1,74 @@
|
||||
import grabDirNames from "../../utils/grab-dir-names";
|
||||
import isDevelopment from "../../utils/is-development";
|
||||
import path from "path";
|
||||
import { rmSync, mkdirSync, writeFileSync } from "fs";
|
||||
const { BUNEXT_VENDOR_DIR, BUNX_CWD_DIR } = grabDirNames();
|
||||
const VENDOR_ENTRIES = {
|
||||
react: `
|
||||
import React from "react";
|
||||
export const {
|
||||
Children, Component, Fragment, Profiler, PureComponent, StrictMode,
|
||||
Suspense, cloneElement, createContext, createElement, createRef,
|
||||
forwardRef, isValidElement, lazy, memo, startTransition,
|
||||
useCallback, useContext, useDebugValue, useDeferredValue, useEffect,
|
||||
useId, useImperativeHandle, useInsertionEffect, useLayoutEffect,
|
||||
useMemo, useReducer, useRef, useState, useSyncExternalStore,
|
||||
useTransition, version, use, cache, act,
|
||||
} = React;
|
||||
export default React;
|
||||
`,
|
||||
"react-dom": `
|
||||
import ReactDOM from "react-dom";
|
||||
export const {
|
||||
createPortal, flushSync, version,
|
||||
} = ReactDOM;
|
||||
export default ReactDOM;
|
||||
`,
|
||||
"react-dom_client": `
|
||||
import ReactDOMClient from "react-dom/client";
|
||||
export const { createRoot, hydrateRoot } = ReactDOMClient;
|
||||
export default ReactDOMClient;
|
||||
`,
|
||||
"react_jsx-runtime": `
|
||||
import JSXRuntime from "react/jsx-runtime";
|
||||
export const { jsx, jsxs, Fragment } = JSXRuntime;
|
||||
`,
|
||||
"react_jsx-dev-runtime": `
|
||||
import JSXDevRuntime from "react/jsx-dev-runtime";
|
||||
export const { jsxDEV, Fragment } = JSXDevRuntime;
|
||||
`,
|
||||
};
|
||||
export default async function bunReactModulesBundler() {
|
||||
const dev = isDevelopment();
|
||||
rmSync(BUNEXT_VENDOR_DIR, { force: true, recursive: true });
|
||||
const tmpDir = path.join(BUNEXT_VENDOR_DIR, "_tmp");
|
||||
mkdirSync(tmpDir, { recursive: true });
|
||||
const entrypoints = [];
|
||||
for (const [name, contents] of Object.entries(VENDOR_ENTRIES)) {
|
||||
const file = path.join(tmpDir, `${name}.mjs`);
|
||||
writeFileSync(file, contents);
|
||||
entrypoints.push(file);
|
||||
}
|
||||
await Bun.build({
|
||||
entrypoints,
|
||||
outdir: BUNEXT_VENDOR_DIR,
|
||||
splitting: true,
|
||||
format: "esm",
|
||||
target: "browser",
|
||||
minify: !dev,
|
||||
define: {
|
||||
"process.env.NODE_ENV": JSON.stringify(dev ? "development" : "production"),
|
||||
},
|
||||
});
|
||||
rmSync(tmpDir, { force: true, recursive: true });
|
||||
const PUBLIC_ROOT = BUNEXT_VENDOR_DIR.replace(BUNX_CWD_DIR, "/.bunext");
|
||||
global.BUNEXT_REACT_IMPORTS_MAP = {
|
||||
imports: {
|
||||
react: `${PUBLIC_ROOT}/react.js`,
|
||||
"react-dom": `${PUBLIC_ROOT}/react-dom.js`,
|
||||
"react-dom/client": `${PUBLIC_ROOT}/react-dom_client.js`,
|
||||
"react/jsx-runtime": `${PUBLIC_ROOT}/react_jsx-runtime.js`,
|
||||
"react/jsx-dev-runtime": `${PUBLIC_ROOT}/react_jsx-dev-runtime.js`,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -2,7 +2,10 @@ import * as esbuild from "esbuild";
|
||||
import type { BundlerCTXMap, PageFiles } from "../../types";
|
||||
type Params = {
|
||||
result: esbuild.BuildResult<esbuild.BuildOptions>;
|
||||
pages: PageFiles[];
|
||||
entryToPage: Map<string, PageFiles & {
|
||||
tsx: string;
|
||||
}>;
|
||||
virtual_match?: string;
|
||||
};
|
||||
export default function grabArtifactsFromBundledResults({ result, pages, }: Params): BundlerCTXMap[] | undefined;
|
||||
export default function grabArtifactsFromBundledResults({ result, entryToPage, virtual_match, }: Params): BundlerCTXMap[] | undefined;
|
||||
export {};
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
import path from "path";
|
||||
import * as esbuild from "esbuild";
|
||||
export default function grabArtifactsFromBundledResults({ result, pages, }) {
|
||||
import grabDirNames from "../../utils/grab-dir-names";
|
||||
import { log } from "../../utils/log";
|
||||
const { ROOT_DIR } = grabDirNames();
|
||||
export default function grabArtifactsFromBundledResults({ result, entryToPage, virtual_match = "hydration-virtual", }) {
|
||||
if (result.errors.length > 0)
|
||||
return;
|
||||
const virtual_regex = new RegExp(`^${virtual_match}:`);
|
||||
const artifacts = Object.entries(result.metafile.outputs)
|
||||
.filter(([, meta]) => meta.entryPoint)
|
||||
.map(([outputPath, meta]) => {
|
||||
const target_page = pages.find((p) => {
|
||||
return meta.entryPoint === `virtual:${p.local_path}`;
|
||||
});
|
||||
const entrypoint = meta.entryPoint?.match(virtual_regex)
|
||||
? meta.entryPoint?.replace(virtual_regex, "")
|
||||
: meta.entryPoint
|
||||
? path.join(ROOT_DIR, meta.entryPoint)
|
||||
: "";
|
||||
const target_page = entryToPage.get(entrypoint);
|
||||
if (!target_page || !meta.entryPoint) {
|
||||
return undefined;
|
||||
}
|
||||
const { file_name, local_path, url_path } = target_page;
|
||||
const cssPath = meta.cssBundle || undefined;
|
||||
return {
|
||||
path: outputPath,
|
||||
hash: path.basename(outputPath, path.extname(outputPath)),
|
||||
@@ -21,7 +27,7 @@ export default function grabArtifactsFromBundledResults({ result, pages, }) {
|
||||
? "text/css"
|
||||
: "text/javascript",
|
||||
entrypoint: meta.entryPoint,
|
||||
css_path: cssPath,
|
||||
css_path: meta.cssBundle,
|
||||
file_name,
|
||||
local_path,
|
||||
url_path,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
type Params = {
|
||||
page_local_path: string;
|
||||
};
|
||||
export default function grabClientHydrationScript({ page_local_path }: Params): string;
|
||||
export default function grabClientHydrationScript({ page_local_path, }: Params): Promise<string | undefined>;
|
||||
export {};
|
||||
|
||||
+34
-15
@@ -1,25 +1,44 @@
|
||||
import { existsSync } from "fs";
|
||||
import path from "path";
|
||||
import grabDirNames from "../../utils/grab-dir-names";
|
||||
import AppNames from "../../utils/grab-app-names";
|
||||
import grabConstants from "../../utils/grab-constants";
|
||||
const { PAGES_DIR } = grabDirNames();
|
||||
export default function grabClientHydrationScript({ page_local_path }) {
|
||||
const { ClientRootElementIDName, ClientRootComponentWindowName } = grabConstants();
|
||||
const root_component_path = path.join(PAGES_DIR, `${AppNames["RootPagesComponentName"]}.tsx`);
|
||||
const does_root_exist = existsSync(root_component_path);
|
||||
import grabRootFilePath from "../server/web-pages/grab-root-file-path";
|
||||
const { ROOT_DIR } = grabDirNames();
|
||||
export default async function grabClientHydrationScript({ page_local_path, }) {
|
||||
const { ClientRootElementIDName, ClientRootComponentWindowName, ClientWindowPagePropsName, } = grabConstants();
|
||||
const { root_file_path } = grabRootFilePath();
|
||||
// const target_path = pagePathTransform({ page_path: page_local_path });
|
||||
// const target_root_path = root_file_path
|
||||
// ? pagePathTransform({ page_path: root_file_path })
|
||||
// : undefined;
|
||||
if (!existsSync(page_local_path)) {
|
||||
return undefined;
|
||||
}
|
||||
if (root_file_path) {
|
||||
if (!existsSync(root_file_path)) {
|
||||
return undefined;
|
||||
}
|
||||
const root_content = await Bun.file(root_file_path).text();
|
||||
if (!root_content.match(/^export default/m)) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
const page_content = await Bun.file(page_local_path).text();
|
||||
if (!page_content.match(/^export default/m)) {
|
||||
return undefined;
|
||||
}
|
||||
let txt = ``;
|
||||
txt += `import { hydrateRoot, createElement } from "react-dom/client";\n`;
|
||||
if (does_root_exist) {
|
||||
txt += `import Root from "${root_component_path}";\n`;
|
||||
txt += `import { hydrateRoot } from "${ROOT_DIR}/node_modules/react-dom/client.js";\n`;
|
||||
// txt += `import react from "${ROOT_DIR}/node_modules/react/index.js";\n`;
|
||||
if (root_file_path) {
|
||||
txt += `import Root from "${root_file_path}";\n`;
|
||||
}
|
||||
txt += `import Page from "${page_local_path}";\n\n`;
|
||||
txt += `const pageProps = window.__PAGE_PROPS__ || {};\n`;
|
||||
if (does_root_exist) {
|
||||
txt += `const component = <Root suppressHydrationWarning={true} {...pageProps}><Page {...pageProps} /></Root>\n`;
|
||||
txt += `const pageProps = window.${ClientWindowPagePropsName} || {};\n`;
|
||||
if (root_file_path) {
|
||||
txt += `const component = <Root {...pageProps}><Page {...pageProps} /></Root>\n`;
|
||||
}
|
||||
else {
|
||||
txt += `const component = <Page suppressHydrationWarning={true} {...pageProps} />\n`;
|
||||
txt += `const component = <Page {...pageProps} />\n`;
|
||||
}
|
||||
txt += `if (window.${ClientRootComponentWindowName}?.render) {\n`;
|
||||
txt += ` window.${ClientRootComponentWindowName}.render(component);\n`;
|
||||
@@ -29,7 +48,7 @@ export default function grabClientHydrationScript({ page_local_path }) {
|
||||
txt += ` } });\n\n`;
|
||||
txt += ` window.${ClientRootComponentWindowName} = root;\n`;
|
||||
txt += ` window.__BUNEXT_RERENDER__ = (NewPage) => {\n`;
|
||||
txt += ` const props = window.__PAGE_PROPS__ || {};\n`;
|
||||
txt += ` const props = window.${ClientWindowPagePropsName} || {};\n`;
|
||||
txt += ` root.render(<NewPage {...props} />);\n`;
|
||||
txt += ` };\n`;
|
||||
txt += `}\n`;
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
type Params = {
|
||||
post_build_fn?: (params: {
|
||||
artifacts: any[];
|
||||
}) => Promise<void> | void;
|
||||
};
|
||||
export default function pagesSSRBundler(params?: Params): Promise<void>;
|
||||
export {};
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
import * as esbuild from "esbuild";
|
||||
import grabAllPages from "../../utils/grab-all-pages";
|
||||
import grabDirNames from "../../utils/grab-dir-names";
|
||||
import isDevelopment from "../../utils/is-development";
|
||||
import tailwindEsbuildPlugin from "../server/web-pages/tailwind-esbuild-plugin";
|
||||
import grabPageReactComponentString from "../server/web-pages/grab-page-react-component-string";
|
||||
import grabRootFilePath from "../server/web-pages/grab-root-file-path";
|
||||
import ssrVirtualFilesPlugin from "./plugins/ssr-virtual-files-plugin";
|
||||
import ssrCTXArtifactTracker from "./plugins/ssr-ctx-artifact-tracker";
|
||||
import { writeFileSync } from "fs";
|
||||
import path from "path";
|
||||
import { log } from "../../utils/log";
|
||||
const { BUNX_CWD_MODULE_CACHE_DIR, BUNX_TMP_DIR } = grabDirNames();
|
||||
export default async function pagesSSRBundler(params) {
|
||||
const pages = grabAllPages({
|
||||
include_server: true,
|
||||
});
|
||||
const dev = isDevelopment();
|
||||
const config = global.BUNEXT_CONFIG;
|
||||
try {
|
||||
writeFileSync(path.join(BUNX_TMP_DIR, "ssr-pages.json"), JSON.stringify(pages, null, 4));
|
||||
}
|
||||
catch (error) { }
|
||||
const entryToPage = new Map();
|
||||
const { root_file_path } = grabRootFilePath();
|
||||
for (const page of pages) {
|
||||
if (page.local_path.match(/\/pages\/api\//) ||
|
||||
page.local_path.match(/\.server\.tsx?$/)) {
|
||||
const ts = await Bun.file(page.local_path).text();
|
||||
if (ts.match(/(export default)|(export \w+ handler)|(export \w+ server)/)) {
|
||||
entryToPage.set(page.local_path, { ...page, tsx: ts });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const tsx = grabPageReactComponentString({
|
||||
file_path: page.local_path,
|
||||
root_file_path,
|
||||
});
|
||||
if (!tsx)
|
||||
continue;
|
||||
if (!tsx.match(/export default/))
|
||||
continue;
|
||||
entryToPage.set(page.local_path, { ...page, tsx });
|
||||
}
|
||||
const entryPoints = [...entryToPage.keys()].map((e) => `ssr-virtual:${e}`);
|
||||
try {
|
||||
writeFileSync(path.join(BUNX_TMP_DIR, "ssr-entry-to-page.json"), JSON.stringify(Object(entryToPage), null, 4));
|
||||
writeFileSync(path.join(BUNX_TMP_DIR, "ssr-entrypoints.json"), JSON.stringify(entryPoints, null, 4));
|
||||
}
|
||||
catch (error) { }
|
||||
try {
|
||||
await esbuild.build({
|
||||
entryPoints,
|
||||
outdir: BUNX_CWD_MODULE_CACHE_DIR,
|
||||
bundle: true,
|
||||
minify: !dev,
|
||||
format: "esm",
|
||||
target: "esnext",
|
||||
platform: "node",
|
||||
define: {
|
||||
"process.env.NODE_ENV": JSON.stringify(dev ? "development" : "production"),
|
||||
},
|
||||
entryNames: "[dir]/[hash]",
|
||||
metafile: true,
|
||||
plugins: [
|
||||
tailwindEsbuildPlugin,
|
||||
ssrVirtualFilesPlugin({
|
||||
entryToPage,
|
||||
}),
|
||||
ssrCTXArtifactTracker({
|
||||
entryToPage,
|
||||
post_build_fn: params?.post_build_fn,
|
||||
}),
|
||||
],
|
||||
jsx: "automatic",
|
||||
external: [
|
||||
"react",
|
||||
"react-dom",
|
||||
"react/jsx-runtime",
|
||||
"react/jsx-dev-runtime",
|
||||
"bun:*",
|
||||
"bun",
|
||||
"sqlite-vec",
|
||||
"better-sqlite3",
|
||||
...(config.ssr_compiler_excludes || []),
|
||||
],
|
||||
splitting: true,
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
global.BUNEXT_SSR_BUNDLER_CTX_DISPOSED = true;
|
||||
log.error(`SSR Bundler Error: ${error}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
type Params = {
|
||||
post_build_fn?: (params: {
|
||||
artifacts: any[];
|
||||
}) => Promise<void> | void;
|
||||
};
|
||||
export default function pagesSSRContextBundler(params?: Params): Promise<void>;
|
||||
export {};
|
||||
@@ -0,0 +1,69 @@
|
||||
import * as esbuild from "esbuild";
|
||||
import grabAllPages from "../../utils/grab-all-pages";
|
||||
import grabDirNames from "../../utils/grab-dir-names";
|
||||
import isDevelopment from "../../utils/is-development";
|
||||
import tailwindEsbuildPlugin from "../server/web-pages/tailwind-esbuild-plugin";
|
||||
import grabPageReactComponentString from "../server/web-pages/grab-page-react-component-string";
|
||||
import grabRootFilePath from "../server/web-pages/grab-root-file-path";
|
||||
import ssrVirtualFilesPlugin from "./plugins/ssr-virtual-files-plugin";
|
||||
import ssrCTXArtifactTracker from "./plugins/ssr-ctx-artifact-tracker";
|
||||
const { BUNX_CWD_MODULE_CACHE_DIR } = grabDirNames();
|
||||
export default async function pagesSSRContextBundler(params) {
|
||||
const pages = grabAllPages();
|
||||
const dev = isDevelopment();
|
||||
if (global.BUNEXT_SSR_BUNDLER_CTX) {
|
||||
await global.BUNEXT_SSR_BUNDLER_CTX.dispose();
|
||||
global.BUNEXT_SSR_BUNDLER_CTX = undefined;
|
||||
}
|
||||
const entryToPage = new Map();
|
||||
const { root_file_path } = grabRootFilePath();
|
||||
for (const page of pages) {
|
||||
if (page.local_path.match(/\/pages\/api\//)) {
|
||||
const ts = await Bun.file(page.local_path).text();
|
||||
entryToPage.set(page.local_path, { ...page, tsx: ts });
|
||||
continue;
|
||||
}
|
||||
const tsx = grabPageReactComponentString({
|
||||
file_path: page.local_path,
|
||||
root_file_path,
|
||||
});
|
||||
if (!tsx)
|
||||
continue;
|
||||
entryToPage.set(page.local_path, { ...page, tsx });
|
||||
}
|
||||
const entryPoints = [...entryToPage.keys()].map((e) => `ssr-virtual:${e}`);
|
||||
global.BUNEXT_SSR_BUNDLER_CTX = await esbuild.context({
|
||||
entryPoints,
|
||||
outdir: BUNX_CWD_MODULE_CACHE_DIR,
|
||||
bundle: true,
|
||||
minify: !dev,
|
||||
format: "esm",
|
||||
target: "esnext",
|
||||
platform: "node",
|
||||
define: {
|
||||
"process.env.NODE_ENV": JSON.stringify(dev ? "development" : "production"),
|
||||
},
|
||||
entryNames: "[dir]/[hash]",
|
||||
metafile: true,
|
||||
plugins: [
|
||||
tailwindEsbuildPlugin,
|
||||
ssrVirtualFilesPlugin({
|
||||
entryToPage,
|
||||
}),
|
||||
ssrCTXArtifactTracker({
|
||||
entryToPage,
|
||||
post_build_fn: params?.post_build_fn,
|
||||
}),
|
||||
],
|
||||
jsx: "automatic",
|
||||
external: [
|
||||
"react",
|
||||
"react-dom",
|
||||
"react/jsx-runtime",
|
||||
"react/jsx-dev-runtime",
|
||||
"bun:*",
|
||||
],
|
||||
// logLevel: "silent",
|
||||
});
|
||||
await global.BUNEXT_SSR_BUNDLER_CTX.rebuild();
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { type Plugin } from "esbuild";
|
||||
import type { PageFiles } from "../../../types";
|
||||
type Params = {
|
||||
pages: PageFiles[];
|
||||
};
|
||||
export default function apiRoutesCTXArtifactTracker({ pages }: Params): Plugin;
|
||||
export {};
|
||||
@@ -0,0 +1,66 @@
|
||||
import {} from "esbuild";
|
||||
import buildOnstartErrorHandler from "../build-on-start-error-handler";
|
||||
import path from "path";
|
||||
import grabDirNames from "../../../utils/grab-dir-names";
|
||||
import { log } from "../../../utils/log";
|
||||
let build_start = 0;
|
||||
let build_starts = 0;
|
||||
const MAX_BUILD_STARTS = 2;
|
||||
const { ROOT_DIR } = grabDirNames();
|
||||
export default function apiRoutesCTXArtifactTracker({ pages }) {
|
||||
const artifactTracker = {
|
||||
name: "ssr-artifact-tracker",
|
||||
setup(build) {
|
||||
build.onStart(async () => {
|
||||
build_starts++;
|
||||
build_start = performance.now();
|
||||
if (build_starts == MAX_BUILD_STARTS) {
|
||||
await buildOnstartErrorHandler();
|
||||
}
|
||||
});
|
||||
build.onEnd((result) => {
|
||||
if (result.errors.length > 0) {
|
||||
console.log("result.errors", result.errors);
|
||||
return;
|
||||
}
|
||||
const artifacts = Object.entries(result.metafile.outputs)
|
||||
.filter(([, meta]) => meta.entryPoint)
|
||||
.map(([outputPath, meta]) => {
|
||||
const entrypoint = meta.entryPoint
|
||||
? path.join(ROOT_DIR, meta.entryPoint)
|
||||
: undefined;
|
||||
const target_page = pages.find((p) => p.local_path == entrypoint);
|
||||
if (!target_page || !meta.entryPoint) {
|
||||
return undefined;
|
||||
}
|
||||
const { file_name, local_path, url_path } = target_page;
|
||||
return {
|
||||
path: outputPath,
|
||||
hash: path.basename(outputPath, path.extname(outputPath)),
|
||||
type: "text/javascript",
|
||||
entrypoint: meta.entryPoint,
|
||||
file_name,
|
||||
local_path,
|
||||
url_path,
|
||||
};
|
||||
});
|
||||
// if (artifacts?.[0] && artifacts.length > 0) {
|
||||
// for (let i = 0; i < artifacts.length; i++) {
|
||||
// const artifact = artifacts[i];
|
||||
// if (
|
||||
// artifact?.local_path &&
|
||||
// global.API_ROUTES_BUNDLER_CTX_MAP
|
||||
// ) {
|
||||
// global.API_ROUTES_BUNDLER_CTX_MAP[
|
||||
// artifact.local_path
|
||||
// ] = artifact;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
const elapsed = (performance.now() - build_start).toFixed(0);
|
||||
log.success(`API Routes [Built] in ${elapsed}ms`);
|
||||
});
|
||||
},
|
||||
};
|
||||
return artifactTracker;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
declare const BunSkipNonBrowserPlugin: Bun.BunPlugin;
|
||||
export default BunSkipNonBrowserPlugin;
|
||||
@@ -0,0 +1,76 @@
|
||||
import { log } from "../../../utils/log";
|
||||
const BunSkipNonBrowserPlugin = {
|
||||
name: "skip-non-browser",
|
||||
setup(build) {
|
||||
const skipFilter = /^(bun:|node:|fs$|path$|os$|crypto$|net$|events$|util$|tls$|url$|process$)/;
|
||||
// const skipped_modules = new Set<string>();
|
||||
build.onResolve({ filter: skipFilter }, (args) => {
|
||||
global.BUNEXT_SKIPPED_BROWSER_MODULES.add(args.path);
|
||||
return {
|
||||
path: args.path,
|
||||
namespace: "skipped",
|
||||
// external: true,
|
||||
};
|
||||
});
|
||||
// build.onEnd(() => {
|
||||
// log.warn(`global.BUNEXT_SKIPPED_BROWSER_MODULES`, [
|
||||
// ...global.BUNEXT_SKIPPED_BROWSER_MODULES,
|
||||
// ]);
|
||||
// });
|
||||
// build.onResolve({ filter: /^[^./]/ }, (args) => {
|
||||
// // If it's a built-in like 'fs' or 'path', skip it immediately
|
||||
// const excludes = [
|
||||
// "fs",
|
||||
// "path",
|
||||
// "os",
|
||||
// "crypto",
|
||||
// "net",
|
||||
// "events",
|
||||
// "util",
|
||||
// "tls",
|
||||
// ];
|
||||
// if (excludes.includes(args.path) || args.path.startsWith("node:")) {
|
||||
// return {
|
||||
// path: args.path,
|
||||
// // namespace: "skipped",
|
||||
// external: true,
|
||||
// };
|
||||
// }
|
||||
// try {
|
||||
// Bun.resolveSync(args.path, args.importer || process.cwd());
|
||||
// return null;
|
||||
// } catch (e) {
|
||||
// console.warn(`[Skip] Mark as external: ${args.path}`);
|
||||
// return {
|
||||
// path: args.path,
|
||||
// // namespace: "skipped",
|
||||
// external: true,
|
||||
// };
|
||||
// }
|
||||
// });
|
||||
build.onLoad({ filter: /.*/, namespace: "skipped" }, (args) => {
|
||||
return {
|
||||
contents: `
|
||||
const proxy = new Proxy(() => proxy, {
|
||||
get: () => proxy,
|
||||
construct: () => proxy,
|
||||
});
|
||||
|
||||
export const Database = proxy;
|
||||
export const join = proxy;
|
||||
export const fileURLToPath = proxy;
|
||||
export const arch = proxy;
|
||||
export const platform = proxy;
|
||||
export const statSync = proxy;
|
||||
|
||||
export const $H = proxy;
|
||||
export const _ = proxy;
|
||||
|
||||
export default proxy;
|
||||
`,
|
||||
loader: "js",
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
export default BunSkipNonBrowserPlugin;
|
||||
@@ -0,0 +1,2 @@
|
||||
import * as esbuild from "esbuild";
|
||||
export default function reactVendorChunkPlugin(): esbuild.Plugin;
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
// plugins/react-vendor-chunk-plugin.ts
|
||||
import * as esbuild from "esbuild";
|
||||
import path from "path";
|
||||
import grabDirNames from "../../../utils/grab-dir-names";
|
||||
const { BUNEXT_VENDOR_DIR } = grabDirNames();
|
||||
const REACT_MODULES = new Set([
|
||||
"react",
|
||||
"react-dom",
|
||||
"react-dom/client",
|
||||
"react/jsx-runtime",
|
||||
"react/jsx-dev-runtime",
|
||||
]);
|
||||
const VENDOR_BASE = "/.bunext/public/vendor";
|
||||
const REACT_ENTRIES = {
|
||||
react: `
|
||||
import React from "react";
|
||||
export const {
|
||||
Children, Component, Fragment, Profiler, PureComponent, StrictMode,
|
||||
Suspense, cloneElement, createContext, createElement, createFactory,
|
||||
createRef, forwardRef, isValidElement, lazy, memo, startTransition,
|
||||
useCallback, useContext, useDebugValue, useDeferredValue, useEffect,
|
||||
useId, useImperativeHandle, useInsertionEffect, useLayoutEffect,
|
||||
useMemo, useReducer, useRef, useState, useSyncExternalStore,
|
||||
useTransition, version,
|
||||
} = React;
|
||||
export default React;
|
||||
`,
|
||||
"react-dom": `
|
||||
import ReactDOM from "react-dom";
|
||||
export const {
|
||||
createPortal, flushSync, findDOMNode, hydrate, render,
|
||||
unmountComponentAtNode, version,
|
||||
} = ReactDOM;
|
||||
export default ReactDOM;
|
||||
`,
|
||||
"react-dom/client": `
|
||||
import ReactDOMClient from "react-dom/client";
|
||||
export const { createRoot, hydrateRoot } = ReactDOMClient;
|
||||
export default ReactDOMClient;
|
||||
`,
|
||||
"react/jsx-runtime": `
|
||||
import JSXRuntime from "react/jsx-runtime";
|
||||
export const { jsx, jsxs, Fragment } = JSXRuntime;
|
||||
export default JSXRuntime;
|
||||
`,
|
||||
"react/jsx-dev-runtime": `
|
||||
import JSXDevRuntime from "react/jsx-dev-runtime";
|
||||
export const { jsxDEV, Fragment } = JSXDevRuntime;
|
||||
export default JSXDevRuntime;
|
||||
`,
|
||||
};
|
||||
// Map bare specifier -> browser path
|
||||
function vendorPath(specifier) {
|
||||
const filename = specifier.replace(/\//g, "_") + ".js";
|
||||
return `${VENDOR_BASE}/${filename}`;
|
||||
}
|
||||
function vendorOutfile(specifier) {
|
||||
const filename = specifier.replace(/\//g, "_") + ".js";
|
||||
return path.join(BUNEXT_VENDOR_DIR, filename);
|
||||
}
|
||||
export default function reactVendorChunkPlugin() {
|
||||
let vendorReady;
|
||||
return {
|
||||
name: "react-vendor-chunk",
|
||||
setup(build) {
|
||||
vendorReady ??= buildAllVendorChunks(build.initialOptions);
|
||||
build.onResolve({ filter: /^react(-dom)?(\/.*)?$/ }, async (args) => {
|
||||
const bare = args.path.replace(/\/index(\.m?js)?$/, "");
|
||||
if (!(bare in REACT_ENTRIES))
|
||||
return;
|
||||
await vendorReady;
|
||||
return {
|
||||
path: vendorPath(bare),
|
||||
external: true,
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
async function buildAllVendorChunks(parentOptions) {
|
||||
await Promise.all(Object.entries(REACT_ENTRIES).map(([specifier, contents]) => esbuild.build({
|
||||
stdin: {
|
||||
contents,
|
||||
resolveDir: process.cwd(),
|
||||
loader: "tsx",
|
||||
},
|
||||
outfile: vendorOutfile(specifier),
|
||||
bundle: true,
|
||||
minify: parentOptions.minify,
|
||||
format: "esm",
|
||||
target: parentOptions.target,
|
||||
platform: "browser",
|
||||
define: parentOptions.define,
|
||||
mainFields: ["module", "main"],
|
||||
conditions: ["import", "default"],
|
||||
})));
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { type Plugin } from "esbuild";
|
||||
import type { PageFiles } from "../../../types";
|
||||
type Params = {
|
||||
entryToPage: Map<string, PageFiles & {
|
||||
tsx: string;
|
||||
}>;
|
||||
post_build_fn?: (params: {
|
||||
artifacts: any[];
|
||||
}) => Promise<void> | void;
|
||||
build_only?: boolean;
|
||||
};
|
||||
export default function esbuildCTXArtifactTracker({ entryToPage, post_build_fn, build_only, }: Params): Plugin;
|
||||
export {};
|
||||
@@ -0,0 +1,104 @@
|
||||
import {} from "esbuild";
|
||||
import { log } from "../../../utils/log";
|
||||
import grabArtifactsFromBundledResults from "../grab-artifacts-from-bundled-result";
|
||||
import buildOnstartErrorHandler from "../build-on-start-error-handler";
|
||||
import _ from "lodash";
|
||||
import pagesSSRBundler from "../pages-ssr-bundler";
|
||||
import grabDirNames from "../../../utils/grab-dir-names";
|
||||
import { cpSync, existsSync, mkdirSync, rmSync } from "fs";
|
||||
import fullRebuild from "../../server/full-rebuild";
|
||||
import path from "path";
|
||||
import cleanupLogsDirs from "../../cleanup-logs-dir";
|
||||
const { BUNX_BUNDLER_ERROR_EXIT_FILE, BUNX_ERROR_LOGS_DIR } = grabDirNames();
|
||||
let build_start = 0;
|
||||
const MAX_BUILD_STARTS = 2;
|
||||
export default function esbuildCTXArtifactTracker({ entryToPage, post_build_fn, build_only, }) {
|
||||
const artifactTracker = {
|
||||
name: "artifact-tracker",
|
||||
setup(build) {
|
||||
build.onStart(async () => {
|
||||
global.BUNEXT_MAIN_CTX_BUILD_STARTS++;
|
||||
build_start = performance.now();
|
||||
const does_error_file_exist = existsSync(BUNX_BUNDLER_ERROR_EXIT_FILE);
|
||||
if (global.BUNEXT_MAIN_CTX_BUILD_STARTS >= MAX_BUILD_STARTS &&
|
||||
!does_error_file_exist) {
|
||||
await buildOnstartErrorHandler();
|
||||
}
|
||||
});
|
||||
build.onEnd(async (result) => {
|
||||
if (result.errors.length > 0) {
|
||||
global.BUNEXT_RECOMPILING = false;
|
||||
global.BUNEXT_IS_SERVER_COMPONENT = false;
|
||||
log.error(`Build errors:`);
|
||||
for (const err of result.errors) {
|
||||
log.error(` ${err.text}${err.location ? ` (${err.location.file}:${err.location.line}:${err.location.column})` : ""}`);
|
||||
}
|
||||
for (let i = global.BUNEXT_HMR_CONTROLLERS.length - 1; i >= 0; i--) {
|
||||
const controller = global.BUNEXT_HMR_CONTROLLERS[i];
|
||||
try {
|
||||
controller?.controller?.enqueue(`event: update\ndata: ${JSON.stringify({ reload: true })}\n\n`);
|
||||
}
|
||||
catch {
|
||||
global.BUNEXT_HMR_CONTROLLERS.splice(i, 1);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
const artifacts = grabArtifactsFromBundledResults({
|
||||
result,
|
||||
entryToPage,
|
||||
});
|
||||
if (artifacts?.[0] && artifacts.length > 0) {
|
||||
for (let i = 0; i < artifacts.length; i++) {
|
||||
const artifact = artifacts[i];
|
||||
if (artifact?.local_path &&
|
||||
global.BUNEXT_BUNDLER_CTX_MAP) {
|
||||
global.BUNEXT_BUNDLER_CTX_MAP[artifact.local_path] =
|
||||
_.merge(global.BUNEXT_BUNDLER_CTX_MAP[artifact.local_path], artifact);
|
||||
}
|
||||
}
|
||||
}
|
||||
const elapsed = (performance.now() - build_start).toFixed(0);
|
||||
log.success(`[Built] in ${elapsed}ms`);
|
||||
global.BUNEXT_MAIN_CTX_BUILD_STARTS = 0;
|
||||
global.BUNEXT_BUNDLER_CTX_DISPOSED = false;
|
||||
const does_error_file_exist = existsSync(BUNX_BUNDLER_ERROR_EXIT_FILE);
|
||||
// SSR must finish before HMR so server props are fresh
|
||||
if (build_only) {
|
||||
try {
|
||||
await pagesSSRBundler();
|
||||
}
|
||||
catch (error) {
|
||||
log.error(`SSR Bundler Error: ${error}`);
|
||||
}
|
||||
}
|
||||
else if (does_error_file_exist) {
|
||||
mkdirSync(BUNX_ERROR_LOGS_DIR, { recursive: true });
|
||||
cpSync(BUNX_BUNDLER_ERROR_EXIT_FILE, path.join(BUNX_ERROR_LOGS_DIR, `${Date.now()}.log`));
|
||||
rmSync(BUNX_BUNDLER_ERROR_EXIT_FILE, { force: true });
|
||||
cleanupLogsDirs();
|
||||
await fullRebuild();
|
||||
}
|
||||
else {
|
||||
try {
|
||||
await pagesSSRBundler();
|
||||
}
|
||||
catch (error) {
|
||||
log.error(`SSR Bundler Error: ${error}`);
|
||||
}
|
||||
if (artifacts?.[0] && artifacts.length > 0) {
|
||||
try {
|
||||
await post_build_fn?.({ artifacts });
|
||||
}
|
||||
catch (error) {
|
||||
log.error(`Post-build Error: ${error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
global.BUNEXT_RECOMPILING = false;
|
||||
global.BUNEXT_IS_SERVER_COMPONENT = false;
|
||||
});
|
||||
},
|
||||
};
|
||||
return artifactTracker;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import type { Plugin } from "esbuild";
|
||||
declare const reactAliasPlugin: Plugin;
|
||||
export default reactAliasPlugin;
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import path from "path";
|
||||
import grabDirNames from "../../../utils/grab-dir-names";
|
||||
const { ROOT_DIR } = grabDirNames();
|
||||
const reactAliasPlugin = {
|
||||
name: "react-alias",
|
||||
setup(build) {
|
||||
const reactPath = path.join(ROOT_DIR, "node_modules");
|
||||
build.onResolve({ filter: /^react$/ }, () => ({
|
||||
path: path.join(reactPath, "react", "index.js"),
|
||||
}));
|
||||
build.onResolve({ filter: /^react-dom$/ }, () => ({
|
||||
path: path.join(reactPath, "react-dom", "index.js"),
|
||||
}));
|
||||
build.onResolve({ filter: /^react\/jsx-runtime$/ }, () => ({
|
||||
path: path.join(reactPath, "react", "jsx-runtime.js"),
|
||||
}));
|
||||
build.onResolve({ filter: /^react\/jsx-dev-runtime$/ }, () => ({
|
||||
path: path.join(reactPath, "react", "jsx-dev-runtime.js"),
|
||||
}));
|
||||
},
|
||||
};
|
||||
export default reactAliasPlugin;
|
||||
@@ -0,0 +1,12 @@
|
||||
import { type Plugin } from "esbuild";
|
||||
import type { PageFiles } from "../../../types";
|
||||
type Params = {
|
||||
entryToPage: Map<string, PageFiles & {
|
||||
tsx: string;
|
||||
}>;
|
||||
post_build_fn?: (params: {
|
||||
artifacts: any[];
|
||||
}) => Promise<void> | void;
|
||||
};
|
||||
export default function ssrCTXArtifactTracker({ entryToPage, post_build_fn, }: Params): Plugin;
|
||||
export {};
|
||||
@@ -0,0 +1,65 @@
|
||||
import {} from "esbuild";
|
||||
import grabArtifactsFromBundledResults from "../grab-artifacts-from-bundled-result";
|
||||
import { writeFileSync } from "fs";
|
||||
import path from "path";
|
||||
import grabDirNames from "../../../utils/grab-dir-names";
|
||||
let build_start = 0;
|
||||
let build_starts = 0;
|
||||
const MAX_BUILD_STARTS = 2;
|
||||
const { BUNX_TMP_DIR } = grabDirNames();
|
||||
export default function ssrCTXArtifactTracker({ entryToPage, post_build_fn, }) {
|
||||
const artifactTracker = {
|
||||
name: "ssr-artifact-tracker",
|
||||
setup(build) {
|
||||
build.onStart(async () => {
|
||||
build_starts++;
|
||||
build_start = performance.now();
|
||||
if (build_starts == MAX_BUILD_STARTS) {
|
||||
global.BUNEXT_SSR_BUNDLER_CTX_DISPOSED = true;
|
||||
await global.BUNEXT_SSR_BUNDLER_CTX?.dispose();
|
||||
global.BUNEXT_SSR_BUNDLER_CTX = undefined;
|
||||
}
|
||||
});
|
||||
build.onEnd(async (result) => {
|
||||
if (result.errors.length > 0) {
|
||||
global.BUNEXT_SSR_BUNDLER_CTX_DISPOSED = true;
|
||||
try {
|
||||
await global.BUNEXT_SSR_BUNDLER_CTX?.dispose();
|
||||
}
|
||||
catch { }
|
||||
global.BUNEXT_SSR_BUNDLER_CTX = undefined;
|
||||
build_starts = 0;
|
||||
for (const err of result.errors) {
|
||||
console.error(`SSR Build error: ${err.text}${err.location ? ` (${err.location.file}:${err.location.line}:${err.location.column})` : ""}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const artifacts = grabArtifactsFromBundledResults({
|
||||
result,
|
||||
entryToPage,
|
||||
virtual_match: `ssr-virtual`,
|
||||
});
|
||||
if (artifacts?.[0] && artifacts.length > 0) {
|
||||
for (let i = 0; i < artifacts.length; i++) {
|
||||
const artifact = artifacts[i];
|
||||
if (artifact?.local_path &&
|
||||
global.BUNEXT_SSR_BUNDLER_CTX_MAP) {
|
||||
global.BUNEXT_SSR_BUNDLER_CTX_MAP[artifact.local_path] = artifact;
|
||||
}
|
||||
}
|
||||
// post_build_fn?.({ artifacts });
|
||||
// const elapsed = (performance.now() - build_start).toFixed(
|
||||
// 0,
|
||||
// );
|
||||
// log.success(`SSR [Built] in ${elapsed}ms`);
|
||||
}
|
||||
try {
|
||||
writeFileSync(path.join(BUNX_TMP_DIR, "ctx-map.json"), JSON.stringify(global.BUNEXT_SSR_BUNDLER_CTX_MAP, null, 4));
|
||||
}
|
||||
catch (error) { }
|
||||
global.BUNEXT_SSR_BUNDLER_CTX_DISPOSED = false;
|
||||
});
|
||||
},
|
||||
};
|
||||
return artifactTracker;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { Plugin } from "esbuild";
|
||||
import type { PageFiles } from "../../../types";
|
||||
type Params = {
|
||||
entryToPage: Map<string, PageFiles & {
|
||||
tsx: string;
|
||||
}>;
|
||||
};
|
||||
export default function ssrVirtualFilesPlugin({ entryToPage }: Params): Plugin;
|
||||
export {};
|
||||
@@ -0,0 +1,31 @@
|
||||
import path from "path";
|
||||
import { log } from "../../../utils/log";
|
||||
export default function ssrVirtualFilesPlugin({ entryToPage }) {
|
||||
const virtualPlugin = {
|
||||
name: "ssr-virtual-hydration",
|
||||
setup(build) {
|
||||
build.onResolve({ filter: /^ssr-virtual:/ }, (args) => {
|
||||
const final_path = args.path.replace(/ssr-virtual:/, "");
|
||||
return {
|
||||
path: final_path,
|
||||
namespace: "ssr-virtual",
|
||||
};
|
||||
});
|
||||
build.onLoad({ filter: /.*/, namespace: "ssr-virtual" }, (args) => {
|
||||
const target = entryToPage.get(args.path);
|
||||
if (!target?.tsx)
|
||||
return null;
|
||||
const contents = target.tsx;
|
||||
if (!contents.match(/export/)) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
contents: contents || "",
|
||||
loader: "tsx",
|
||||
resolveDir: path.dirname(target.local_path),
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
return virtualPlugin;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { Plugin } from "esbuild";
|
||||
import type { PageFiles } from "../../../types";
|
||||
type Params = {
|
||||
entryToPage: Map<string, PageFiles & {
|
||||
tsx: string;
|
||||
}>;
|
||||
};
|
||||
export default function virtualFilesPlugin({ entryToPage }: Params): Plugin;
|
||||
export {};
|
||||
@@ -0,0 +1,39 @@
|
||||
import path from "path";
|
||||
export default function virtualFilesPlugin({ entryToPage }) {
|
||||
const virtualPlugin = {
|
||||
name: "virtual-hydration",
|
||||
setup(build) {
|
||||
build.onResolve({ filter: /^hydration-virtual:/ }, (args) => {
|
||||
const final_path = args.path.replace(/hydration-virtual:/, "");
|
||||
return {
|
||||
path: final_path,
|
||||
namespace: "hydration-virtual",
|
||||
};
|
||||
});
|
||||
build.onResolve({ filter: /node_modules\/react(-dom)?/ }, (args) => ({
|
||||
path: args.path.includes("react-dom")
|
||||
? args.path.includes("client")
|
||||
? "react-dom/client"
|
||||
: "react-dom"
|
||||
: args.path.includes("jsx-dev")
|
||||
? "react/jsx-dev-runtime"
|
||||
: args.path.includes("jsx")
|
||||
? "react/jsx-runtime"
|
||||
: "react",
|
||||
external: true,
|
||||
}));
|
||||
build.onLoad({ filter: /.*/, namespace: "hydration-virtual" }, (args) => {
|
||||
const target = entryToPage.get(args.path);
|
||||
if (!target?.tsx)
|
||||
return null;
|
||||
const contents = target.tsx;
|
||||
return {
|
||||
contents: contents || "",
|
||||
loader: "tsx",
|
||||
resolveDir: path.dirname(target.local_path),
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
return virtualPlugin;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export default function reactModulesBundler(): Promise<void>;
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
import * as esbuild from "esbuild";
|
||||
import grabDirNames from "../../utils/grab-dir-names";
|
||||
import isDevelopment from "../../utils/is-development";
|
||||
import path from "path";
|
||||
import { rmSync, mkdirSync, writeFileSync } from "fs";
|
||||
const { BUNEXT_VENDOR_DIR, BUNX_CWD_DIR, ROOT_DIR } = grabDirNames();
|
||||
const VENDOR_ENTRIES = {
|
||||
react: `
|
||||
import React from "react";
|
||||
export const {
|
||||
Children, Component, Fragment, Profiler, PureComponent, StrictMode,
|
||||
Suspense, cloneElement, createContext, createElement, createRef,
|
||||
forwardRef, isValidElement, lazy, memo, startTransition,
|
||||
useCallback, useContext, useDebugValue, useDeferredValue, useEffect,
|
||||
useId, useImperativeHandle, useInsertionEffect, useLayoutEffect,
|
||||
useMemo, useReducer, useRef, useState, useSyncExternalStore,
|
||||
useTransition, version, use, cache, act,
|
||||
} = React;
|
||||
export default React;
|
||||
`,
|
||||
"react-dom": `
|
||||
import ReactDOM from "react-dom";
|
||||
export const {
|
||||
createPortal, flushSync, version,
|
||||
} = ReactDOM;
|
||||
export default ReactDOM;
|
||||
`,
|
||||
"react-dom_client": `
|
||||
import ReactDOMClient from "react-dom/client";
|
||||
export const { createRoot, hydrateRoot } = ReactDOMClient;
|
||||
export default ReactDOMClient;
|
||||
`,
|
||||
"react_jsx-runtime": `
|
||||
import JSXRuntime from "react/jsx-runtime";
|
||||
export const { jsx, jsxs, Fragment } = JSXRuntime;
|
||||
`,
|
||||
"react_jsx-dev-runtime": `
|
||||
import JSXDevRuntime from "react/jsx-dev-runtime";
|
||||
export const { jsxDEV, Fragment } = JSXDevRuntime;
|
||||
`,
|
||||
};
|
||||
export default async function reactModulesBundler() {
|
||||
const dev = isDevelopment();
|
||||
rmSync(BUNEXT_VENDOR_DIR, { force: true, recursive: true });
|
||||
const tmpDir = path.join(BUNEXT_VENDOR_DIR, "_tmp");
|
||||
mkdirSync(tmpDir, { recursive: true });
|
||||
const entrypoints = {};
|
||||
for (const [name, contents] of Object.entries(VENDOR_ENTRIES)) {
|
||||
const file = path.join(tmpDir, `${name}.mjs`);
|
||||
writeFileSync(file, contents);
|
||||
entrypoints[name] = file;
|
||||
}
|
||||
await esbuild.build({
|
||||
entryPoints: entrypoints,
|
||||
outdir: BUNEXT_VENDOR_DIR,
|
||||
bundle: true,
|
||||
splitting: true,
|
||||
format: "esm",
|
||||
platform: "browser",
|
||||
target: "es2020",
|
||||
minify: !dev,
|
||||
define: {
|
||||
"process.env.NODE_ENV": JSON.stringify(dev ? "development" : "production"),
|
||||
},
|
||||
});
|
||||
rmSync(tmpDir, { force: true, recursive: true });
|
||||
const PUBLIC_ROOT = BUNEXT_VENDOR_DIR.replace(BUNX_CWD_DIR, "/.bunext");
|
||||
global.BUNEXT_REACT_IMPORTS_MAP = {
|
||||
imports: {
|
||||
react: `${PUBLIC_ROOT}/react.js`,
|
||||
"react-dom": `${PUBLIC_ROOT}/react-dom.js`,
|
||||
"react-dom/client": `${PUBLIC_ROOT}/react-dom_client.js`,
|
||||
"react/jsx-runtime": `${PUBLIC_ROOT}/react_jsx-runtime.js`,
|
||||
"react/jsx-dev-runtime": `${PUBLIC_ROOT}/react_jsx-dev-runtime.js`,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { BundlerCTXMap } from "../../types";
|
||||
type Params = {
|
||||
artifacts: BundlerCTXMap[];
|
||||
page_file_paths?: string[];
|
||||
};
|
||||
export default function recordArtifacts({ artifacts, page_file_paths, }: Params): Promise<void>;
|
||||
export {};
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import grabDirNames from "../../utils/grab-dir-names";
|
||||
import _ from "lodash";
|
||||
const { HYDRATION_DST_DIR_MAP_JSON_FILE } = grabDirNames();
|
||||
export default async function recordArtifacts({ artifacts, page_file_paths, }) {
|
||||
const artifacts_map = {};
|
||||
for (const artifact of artifacts) {
|
||||
if (artifact?.local_path) {
|
||||
artifacts_map[artifact.local_path] = artifact;
|
||||
}
|
||||
}
|
||||
if (global.BUNEXT_BUNDLER_CTX_MAP) {
|
||||
global.BUNEXT_BUNDLER_CTX_MAP = _.merge(global.BUNEXT_BUNDLER_CTX_MAP, artifacts_map);
|
||||
}
|
||||
// await Bun.write(
|
||||
// HYDRATION_DST_DIR_MAP_JSON_FILE,
|
||||
// JSON.stringify(artifacts_map, null, 4),
|
||||
// );
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
type Params = {
|
||||
txt_code: string;
|
||||
file_path: string;
|
||||
};
|
||||
export default function stripServerSideLogic({ txt_code, file_path }: Params): string;
|
||||
export {};
|
||||
@@ -0,0 +1,92 @@
|
||||
import path from "path";
|
||||
import ts from "typescript";
|
||||
export default function stripServerSideLogic({ txt_code, file_path }) {
|
||||
const file_dir_name = path.dirname(file_path);
|
||||
// 1. Initial Parse of the source
|
||||
const sourceFile = ts.createSourceFile("file.tsx", txt_code, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
||||
const printer = ts.createPrinter();
|
||||
/**
|
||||
* PASS 1: Remove 'server' export and resolve absolute paths
|
||||
*/
|
||||
const stripTransformer = (context) => (rootNode) => {
|
||||
const visitor = (node) => {
|
||||
// Remove 'export const server'
|
||||
if (ts.isVariableStatement(node) &&
|
||||
node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) {
|
||||
if (node.declarationList.declarations.some((d) => ts.isIdentifier(d.name) &&
|
||||
d.name.text === "server")) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
// Convert relative imports to absolute
|
||||
if (ts.isImportDeclaration(node)) {
|
||||
const specifier = node.moduleSpecifier;
|
||||
if (ts.isStringLiteral(specifier) &&
|
||||
specifier.text.startsWith(".")) {
|
||||
const absolutePath = path.resolve(file_dir_name, specifier.text);
|
||||
return ts.factory.updateImportDeclaration(node, node.modifiers, node.importClause, ts.factory.createStringLiteral(absolutePath), node.attributes);
|
||||
}
|
||||
}
|
||||
return ts.visitEachChild(node, visitor, context);
|
||||
};
|
||||
return ts.visitNode(rootNode, visitor);
|
||||
};
|
||||
const step1Result = ts.transform(sourceFile, [stripTransformer]);
|
||||
const intermediateCode = printer.printFile(step1Result.transformed[0]);
|
||||
// 2. Re-parse to get a "Clean Slate" AST
|
||||
const cleanSource = ts.createSourceFile("clean.tsx", intermediateCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
||||
/**
|
||||
* PASS 2: Collect all used Identifiers and Prune Imports
|
||||
*/
|
||||
const usedIdentifiers = new Set();
|
||||
function walkAndFindUsages(node) {
|
||||
if (ts.isIdentifier(node)) {
|
||||
// We only care about identifiers that AREN'T the names in the import statements themselves
|
||||
const parent = node.parent;
|
||||
const isImportName = ts.isImportSpecifier(parent) ||
|
||||
ts.isImportClause(parent) ||
|
||||
ts.isNamespaceImport(parent);
|
||||
if (!isImportName) {
|
||||
usedIdentifiers.add(node.text);
|
||||
}
|
||||
}
|
||||
ts.forEachChild(node, walkAndFindUsages);
|
||||
}
|
||||
walkAndFindUsages(cleanSource);
|
||||
const cleanupTransformer = (context) => (rootNode) => {
|
||||
const visitor = (node) => {
|
||||
if (ts.isImportDeclaration(node)) {
|
||||
const clause = node.importClause;
|
||||
if (!clause)
|
||||
return node;
|
||||
// 1. Clean Named Imports: { A, B }
|
||||
if (clause.namedBindings &&
|
||||
ts.isNamedImports(clause.namedBindings)) {
|
||||
const activeElements = clause.namedBindings.elements.filter((el) => usedIdentifiers.has(el.name.text));
|
||||
// If no named imports are used and there is no default import, nix the whole line
|
||||
if (activeElements.length === 0 && !clause.name)
|
||||
return undefined;
|
||||
// If we have some named imports left, update the node
|
||||
return ts.factory.updateImportDeclaration(node, node.modifiers, ts.factory.updateImportClause(clause, clause.isTypeOnly,
|
||||
// Only keep default import if it's used
|
||||
clause.name &&
|
||||
usedIdentifiers.has(clause.name.text)
|
||||
? clause.name
|
||||
: undefined, ts.factory.createNamedImports(activeElements)), node.moduleSpecifier, node.attributes);
|
||||
}
|
||||
// 2. Clean Default Imports: import X from '...'
|
||||
if (clause.name && !usedIdentifiers.has(clause.name.text)) {
|
||||
// If there are no named bindings attached, nix the whole line
|
||||
if (!clause.namedBindings)
|
||||
return undefined;
|
||||
// Otherwise, just strip the default name and keep the named bindings
|
||||
return ts.factory.updateImportDeclaration(node, node.modifiers, ts.factory.updateImportClause(clause, clause.isTypeOnly, undefined, clause.namedBindings), node.moduleSpecifier, node.attributes);
|
||||
}
|
||||
}
|
||||
return ts.visitEachChild(node, visitor, context);
|
||||
};
|
||||
return ts.visitNode(rootNode, visitor);
|
||||
};
|
||||
const finalResult = ts.transform(cleanSource, [cleanupTransformer]);
|
||||
return printer.printFile(finalResult.transformed[0]);
|
||||
}
|
||||
Vendored
+46
-18
@@ -1,26 +1,54 @@
|
||||
import { type Ora } from "ora";
|
||||
import type { BundlerCTXMap, BunextConfig, GlobalHMRControllerObject, PageFiles } from "../types";
|
||||
import type { FileSystemRouter, Server } from "bun";
|
||||
import { type DirNames } from "../utils/grab-dir-names";
|
||||
import type { BuildContext } from "esbuild";
|
||||
import { type FSWatcher } from "fs";
|
||||
import grabConstants from "../utils/grab-constants";
|
||||
import type { FSWatcher } from "fs";
|
||||
/**
|
||||
* # Declare Global Variables
|
||||
*/
|
||||
declare global {
|
||||
var ORA_SPINNER: Ora;
|
||||
var CONFIG: BunextConfig;
|
||||
var SERVER: Server | undefined;
|
||||
var RECOMPILING: boolean;
|
||||
var WATCHER_TIMEOUT: any;
|
||||
var ROUTER: FileSystemRouter;
|
||||
var HMR_CONTROLLERS: GlobalHMRControllerObject[];
|
||||
var LAST_BUILD_TIME: number;
|
||||
var BUNDLER_CTX: BuildContext | undefined;
|
||||
var BUNDLER_CTX_MAP: BundlerCTXMap[] | undefined;
|
||||
var IS_FIRST_BUNDLE_READY: boolean;
|
||||
var BUNDLER_REBUILDS: 0;
|
||||
var PAGES_SRC_WATCHER: FSWatcher | undefined;
|
||||
var CURRENT_VERSION: string | undefined;
|
||||
var PAGE_FILES: PageFiles[];
|
||||
var BUNEXT_CONFIG: BunextConfig;
|
||||
var BUNEXT_SERVER: Server<any> | undefined;
|
||||
var BUNEXT_RECOMPILING: boolean;
|
||||
var BUNEXT_BUILDING_SSR: boolean;
|
||||
var BUNEXT_IS_SERVER_COMPONENT: boolean;
|
||||
var BUNEXT_WATCHER_TIMEOUT: any;
|
||||
var BUNEXT_ROUTER: FileSystemRouter;
|
||||
var BUNEXT_HMR_CONTROLLERS: GlobalHMRControllerObject[];
|
||||
var BUNEXT_LAST_BUILD_TIME: number;
|
||||
var BUNEXT_BUNDLER_CTX_MAP: {
|
||||
[k: string]: BundlerCTXMap;
|
||||
};
|
||||
var BUNEXT_SSR_BUNDLER_CTX_MAP: {
|
||||
[k: string]: BundlerCTXMap;
|
||||
};
|
||||
var BUNEXT_BUNDLER_REBUILDS: 0;
|
||||
var BUNEXT_PAGES_SRC_WATCHER: FSWatcher | undefined;
|
||||
var BUNEXT_CURRENT_VERSION: string | undefined;
|
||||
var BUNEXT_PAGE_FILES: PageFiles[];
|
||||
var BUNEXT_ROOT_FILE_UPDATED: boolean;
|
||||
var BUNEXT_SKIPPED_BROWSER_MODULES: Set<string>;
|
||||
var BUNEXT_BUNDLER_CTX: BuildContext | undefined;
|
||||
var BUNEXT_SSR_BUNDLER_CTX: BuildContext | undefined;
|
||||
var BUNEXT_DIR_NAMES: DirNames;
|
||||
var BUNEXT_REACT_IMPORTS_MAP: {
|
||||
imports: Record<string, string>;
|
||||
};
|
||||
var BUNEXT_REACT_DOM_SERVER: any;
|
||||
var BUNEXT_REACT_DOM_MODULE_CACHE: Map<string, {
|
||||
main: any;
|
||||
css: string;
|
||||
}>;
|
||||
var BUNEXT_BUNDLER_CTX_DISPOSED: boolean | undefined;
|
||||
var BUNEXT_SSR_BUNDLER_CTX_DISPOSED: boolean | undefined;
|
||||
var BUNEXT_REBUILD_RETRIES: number;
|
||||
var BUNEXT_IS_404_PAGE: boolean;
|
||||
var BUNEXT_CONSTANTS: ReturnType<typeof grabConstants>;
|
||||
var BUNEXT_MAIN_CTX_BUILD_STARTS: number;
|
||||
}
|
||||
export default function bunextInit(): Promise<void>;
|
||||
type Params = {
|
||||
build_only?: boolean;
|
||||
};
|
||||
export default function bunextInit(params?: Params): Promise<void>;
|
||||
export {};
|
||||
|
||||
Vendored
+43
-44
@@ -1,61 +1,60 @@
|
||||
import ora, {} from "ora";
|
||||
import grabDirNames from "../utils/grab-dir-names";
|
||||
import { readFileSync } from "fs";
|
||||
import grabDirNames, {} from "../utils/grab-dir-names";
|
||||
import init from "./init";
|
||||
import isDevelopment from "../utils/is-development";
|
||||
import allPagesBundler from "./bundler/all-pages-bundler";
|
||||
import serverPostBuildFn from "./server/server-post-build-fn";
|
||||
import watcher from "./server/watcher";
|
||||
import EJSON from "../utils/ejson";
|
||||
import { log } from "../utils/log";
|
||||
import cron from "./server/cron";
|
||||
export default async function bunextInit() {
|
||||
global.ORA_SPINNER = ora();
|
||||
global.ORA_SPINNER.clear();
|
||||
global.HMR_CONTROLLERS = [];
|
||||
global.IS_FIRST_BUNDLE_READY = false;
|
||||
global.BUNDLER_REBUILDS = 0;
|
||||
global.PAGE_FILES = [];
|
||||
import allPagesESBuildContextBundler from "./bundler/all-pages-esbuild-context-bundler";
|
||||
import serverPostBuildFn from "./server/server-post-build-fn";
|
||||
import reactModulesBundler from "./bundler/react-modules-bundler";
|
||||
import grabConstants from "../utils/grab-constants";
|
||||
import watcherEsbuildCTX from "./server/watcher-esbuild-ctx";
|
||||
const dirNames = grabDirNames();
|
||||
const { PAGES_DIR } = dirNames;
|
||||
export default async function bunextInit(params) {
|
||||
global.BUNEXT_HMR_CONTROLLERS = [];
|
||||
global.BUNEXT_BUNDLER_CTX_MAP = {};
|
||||
global.BUNEXT_SSR_BUNDLER_CTX_MAP = {};
|
||||
// global.BUNEXT_API_ROUTES_BUNDLER_CTX_MAP = {};
|
||||
global.BUNEXT_BUNDLER_REBUILDS = 0;
|
||||
global.BUNEXT_REBUILD_RETRIES = 0;
|
||||
global.BUNEXT_PAGE_FILES = [];
|
||||
global.BUNEXT_SKIPPED_BROWSER_MODULES = new Set();
|
||||
global.BUNEXT_DIR_NAMES = dirNames;
|
||||
global.BUNEXT_REACT_IMPORTS_MAP = { imports: {} };
|
||||
global.BUNEXT_REACT_DOM_MODULE_CACHE = new Map();
|
||||
global.BUNEXT_MAIN_CTX_BUILD_STARTS = 0;
|
||||
await init();
|
||||
log.banner();
|
||||
const { PAGES_DIR, HYDRATION_DST_DIR_MAP_JSON_FILE } = grabDirNames();
|
||||
global.BUNEXT_CONSTANTS = grabConstants();
|
||||
await reactModulesBundler();
|
||||
const router = new Bun.FileSystemRouter({
|
||||
style: "nextjs",
|
||||
dir: PAGES_DIR,
|
||||
});
|
||||
global.ROUTER = router;
|
||||
global.BUNEXT_ROUTER = router;
|
||||
const is_dev = isDevelopment();
|
||||
if (is_dev) {
|
||||
await allPagesBundler({
|
||||
watch: true,
|
||||
post_build_fn: serverPostBuildFn,
|
||||
if (params?.build_only) {
|
||||
log.build(`Building Modules ...`);
|
||||
await allPagesESBuildContextBundler();
|
||||
}
|
||||
else if (is_dev) {
|
||||
log.build(`Building Modules ...`);
|
||||
await allPagesESBuildContextBundler({
|
||||
post_build_fn: async () => {
|
||||
await serverPostBuildFn();
|
||||
},
|
||||
});
|
||||
watcher();
|
||||
watcherEsbuildCTX();
|
||||
}
|
||||
else {
|
||||
const artifacts = EJSON.parse(readFileSync(HYDRATION_DST_DIR_MAP_JSON_FILE, "utf-8"));
|
||||
if (!artifacts?.[0]) {
|
||||
log.error("Please build first.");
|
||||
process.exit(1);
|
||||
}
|
||||
global.BUNDLER_CTX_MAP = artifacts;
|
||||
global.IS_FIRST_BUNDLE_READY = true;
|
||||
log.build(`Building Modules ...`);
|
||||
await allPagesESBuildContextBundler({ start: true });
|
||||
cron();
|
||||
}
|
||||
let bundle_ready_retries = 0;
|
||||
const MAX_BUNDLE_READY_RETRIES = 10;
|
||||
while (!global.IS_FIRST_BUNDLE_READY) {
|
||||
if (bundle_ready_retries > MAX_BUNDLE_READY_RETRIES) {
|
||||
log.error("Couldn't grab first bundle for dev environment");
|
||||
process.exit(1);
|
||||
}
|
||||
bundle_ready_retries++;
|
||||
await Bun.sleep(500);
|
||||
}
|
||||
/**
|
||||
* First Rebuild to Avoid errors
|
||||
*/
|
||||
if (is_dev && global.BUNDLER_CTX) {
|
||||
await global.BUNDLER_CTX.rebuild();
|
||||
}
|
||||
}
|
||||
// process.on("exit", (code) => {
|
||||
// Bun.spawn([process.execPath, ...process.argv.slice(1)], {
|
||||
// stdio: ["inherit", "inherit", "inherit"],
|
||||
// env: process.env,
|
||||
// });
|
||||
// });
|
||||
|
||||
+4
@@ -13,6 +13,10 @@ export default async function trimAllCache() {
|
||||
const trim_key = await trimCacheKey({
|
||||
key: cache_key,
|
||||
});
|
||||
if (trim_key.success) {
|
||||
cached_items.splice(i, 1);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
|
||||
+2
-2
@@ -9,8 +9,8 @@ export default async function trimCacheKey({ key, }) {
|
||||
const { cache_name, cache_meta_name } = grabCacheNames({
|
||||
key,
|
||||
});
|
||||
const config = global.CONFIG;
|
||||
const default_expiry_time_seconds = config.defaultCacheExpiry ||
|
||||
const config = global.BUNEXT_CONFIG;
|
||||
const default_expiry_time_seconds = config.default_cache_expiry ||
|
||||
AppData["DefaultCacheExpiryTimeSeconds"];
|
||||
const default_expiry_time_milliseconds = default_expiry_time_seconds * 1000;
|
||||
const cache_content_path = path.join(BUNEXT_CACHE_DIR, cache_name);
|
||||
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
export default function cleanupLogsDirs(): void;
|
||||
Vendored
+48
@@ -0,0 +1,48 @@
|
||||
import path from "path";
|
||||
import { mkdirSync, readdirSync, rmSync, statSync, writeFileSync } from "fs";
|
||||
import grabDirNames from "../utils/grab-dir-names";
|
||||
import grabConstants from "../utils/grab-constants";
|
||||
import { AppData } from "../data/app-data";
|
||||
const { BUNX_LOGS_DIR } = grabDirNames();
|
||||
export default function cleanupLogsDirs() {
|
||||
const logs_dirs = readdirSync(BUNX_LOGS_DIR);
|
||||
const { config } = grabConstants();
|
||||
const MAX_LOGS = config.max_logs || AppData["DefaultMaxLogs"];
|
||||
for (let i = 0; i < logs_dirs.length; i++) {
|
||||
const dir = logs_dirs[i];
|
||||
const full_path = path.join(BUNX_LOGS_DIR, dir);
|
||||
const path_stats = statSync(full_path);
|
||||
if (!path_stats.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
const sub_dir_files = readdirSync(full_path).sort((a, b) => {
|
||||
const timestamp_a = Number(a.split(".")[0]);
|
||||
const timestamp_b = Number(b.split(".")[0]);
|
||||
if (timestamp_a > timestamp_b)
|
||||
return 1;
|
||||
return -1;
|
||||
});
|
||||
for (let j = 0; j < sub_dir_files.length; j++) {
|
||||
const sub_dir_file = sub_dir_files[j];
|
||||
const sub_dir_file_full_path = path.join(full_path, sub_dir_file);
|
||||
const sub_dir_file_Stats = statSync(sub_dir_file_full_path);
|
||||
if (!sub_dir_file_Stats.isFile()) {
|
||||
rmSync(sub_dir_file_full_path, {
|
||||
force: true,
|
||||
recursive: true,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (j > MAX_LOGS - 1) {
|
||||
rmSync(sub_dir_file_full_path, { force: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// log.info("Running development server ...");
|
||||
// try {
|
||||
// rmSync(HYDRATION_DST_DIR, { recursive: true });
|
||||
// rmSync(BUNX_CWD_PAGES_REWRITE_DIR, { recursive: true });
|
||||
// } catch (error) {}
|
||||
// await bunextInit();
|
||||
// await startServer();
|
||||
Vendored
+23
-7
@@ -1,22 +1,38 @@
|
||||
import { existsSync, mkdirSync, writeFileSync } from "fs";
|
||||
import { existsSync, mkdirSync, rmSync, writeFileSync } from "fs";
|
||||
import grabDirNames from "../utils/grab-dir-names";
|
||||
import { execSync } from "child_process";
|
||||
import path from "path";
|
||||
import grabConfig from "./grab-config";
|
||||
import { log } from "../utils/log";
|
||||
export default async function () {
|
||||
const dirNames = grabDirNames();
|
||||
execSync(`rm -rf ${dirNames.BUNEXT_CACHE_DIR}`);
|
||||
execSync(`rm -rf ${dirNames.BUNX_CWD_MODULE_CACHE_DIR}`);
|
||||
const is_dev = !Boolean(process.env.NODE_ENV == "production");
|
||||
rmSync(dirNames.BUNEXT_CACHE_DIR, {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
rmSync(dirNames.BUNX_CWD_MODULE_CACHE_DIR, {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
if (dirNames.ROOT_DIR.startsWith(dirNames.BUNX_ROOT_DIR) &&
|
||||
!dirNames.ROOT_DIR.includes(`${dirNames.BUNX_ROOT_DIR}/test/`)) {
|
||||
log.error(`Can't Run From this Directory => ${dirNames.ROOT_DIR}`);
|
||||
process.exit(1);
|
||||
}
|
||||
else {
|
||||
}
|
||||
try {
|
||||
const package_json = await Bun.file(path.resolve(__dirname, "../../package.json")).json();
|
||||
const current_version = package_json.version;
|
||||
global.CURRENT_VERSION = current_version;
|
||||
global.BUNEXT_CURRENT_VERSION = current_version;
|
||||
}
|
||||
catch (error) { }
|
||||
const keys = Object.keys(dirNames);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i];
|
||||
const dir = dirNames[key];
|
||||
if (!dir.match(/\//))
|
||||
continue;
|
||||
if (!existsSync(dir) && !dir.match(/\.\w+$/)) {
|
||||
mkdirSync(dir, { recursive: true });
|
||||
continue;
|
||||
@@ -29,8 +45,8 @@ export default async function () {
|
||||
}
|
||||
}
|
||||
const config = (await grabConfig()) || {};
|
||||
global.CONFIG = {
|
||||
global.BUNEXT_CONFIG = {
|
||||
...config,
|
||||
development: true,
|
||||
development: is_dev,
|
||||
};
|
||||
}
|
||||
|
||||
Vendored
+46
@@ -0,0 +1,46 @@
|
||||
import { spawn } from "bun";
|
||||
// Only the "supervisor" respawns. The child sets this env var so it won't respawn itself.
|
||||
const IS_CHILD = process.env.__RESPAWN_CHILD === "1";
|
||||
let shuttingDown = false;
|
||||
async function cleanup() {
|
||||
// Put real cleanup here: close DB handles, servers, file descriptors, timers, etc.
|
||||
// Must be awaitable — do NOT rely on process.on("exit") for this.
|
||||
}
|
||||
function respawn(code) {
|
||||
const child = spawn({
|
||||
cmd: [process.execPath, ...process.argv.slice(1)],
|
||||
stdio: ["inherit", "inherit", "inherit"],
|
||||
env: { ...process.env, __RESPAWN_CHILD: "1" },
|
||||
// Detach so the child survives independently and gets its own process group.
|
||||
// Without this, killing the parent's group can take the child with it.
|
||||
});
|
||||
// Let the child live on its own.
|
||||
child.unref?.();
|
||||
}
|
||||
async function shutdown(code) {
|
||||
if (shuttingDown)
|
||||
return;
|
||||
shuttingDown = true;
|
||||
try {
|
||||
await cleanup();
|
||||
}
|
||||
catch (e) {
|
||||
console.error("cleanup failed:", e);
|
||||
}
|
||||
// Only the supervisor respawns, and only on abnormal exit.
|
||||
if (!IS_CHILD && code !== 0) {
|
||||
respawn(code);
|
||||
}
|
||||
process.exit(code);
|
||||
}
|
||||
// Catch the things that actually fire *before* exit, where async works.
|
||||
process.on("SIGINT", () => shutdown(130));
|
||||
process.on("SIGTERM", () => shutdown(143));
|
||||
process.on("uncaughtException", (err) => {
|
||||
console.error(err);
|
||||
shutdown(1);
|
||||
});
|
||||
process.on("unhandledRejection", (err) => {
|
||||
console.error(err);
|
||||
shutdown(1);
|
||||
});
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
type Params = {
|
||||
req: Request;
|
||||
server: Bun.Server<any>;
|
||||
};
|
||||
export default function bunextRequestHandler({ req: initial_req, }: Params): Promise<Response>;
|
||||
export default function bunextRequestHandler({ req: initial_req, server, }: Params): Promise<Response>;
|
||||
export {};
|
||||
|
||||
+32
-14
@@ -1,21 +1,26 @@
|
||||
import handleWebPages from "./web-pages/handle-web-pages";
|
||||
import handleRoutes from "./handle-routes";
|
||||
import isDevelopment from "../../utils/is-development";
|
||||
import grabConstants from "../../utils/grab-constants";
|
||||
import { AppData } from "../../data/app-data";
|
||||
import handleHmr from "./handle-hmr";
|
||||
import handleHmrUpdate from "./handle-hmr-update";
|
||||
import handlePublic from "./handle-public";
|
||||
import handleFiles from "./handle-files";
|
||||
export default async function bunextRequestHandler({ req: initial_req, }) {
|
||||
import handleBunextPublicAssets from "./handle-bunext-public-assets";
|
||||
import checkExcludedPatterns from "../../utils/check-excluded-patterns";
|
||||
import { AppData } from "../../data/app-data";
|
||||
import fullRebuild from "./full-rebuild";
|
||||
const HMR_RETRY_COOLDOWN_MS = 5000;
|
||||
let lastHmrRetryTime = 0;
|
||||
export default async function bunextRequestHandler({ req: initial_req, server, }) {
|
||||
const is_dev = isDevelopment();
|
||||
let req = initial_req.clone();
|
||||
try {
|
||||
const url = new URL(req.url);
|
||||
const { config } = grabConstants();
|
||||
if (checkExcludedPatterns({ path: url.pathname })) {
|
||||
return Response.json({ success: false, msg: `Invalid Path` });
|
||||
}
|
||||
let response = undefined;
|
||||
if (config?.middleware) {
|
||||
const middleware_res = await config.middleware({
|
||||
if (global.BUNEXT_CONSTANTS.config?.middleware) {
|
||||
const middleware_res = await global.BUNEXT_CONSTANTS.config.middleware({
|
||||
req: initial_req,
|
||||
url,
|
||||
});
|
||||
@@ -26,11 +31,20 @@ export default async function bunextRequestHandler({ req: initial_req, }) {
|
||||
req = middleware_res;
|
||||
}
|
||||
}
|
||||
if (url.pathname == `/${AppData["ClientHMRPath"]}`) {
|
||||
response = await handleHmrUpdate({ req });
|
||||
if (is_dev && url.pathname == AppData["BunextHMRRetryRoute"]) {
|
||||
const now = Date.now();
|
||||
if (now - lastHmrRetryTime < HMR_RETRY_COOLDOWN_MS) {
|
||||
return new Response("Too Many Requests", { status: 429 });
|
||||
}
|
||||
lastHmrRetryTime = now;
|
||||
await fullRebuild({ msg: `HMR Retry Rebuild ...` });
|
||||
return new Response("Modules Rebuilt");
|
||||
}
|
||||
else if (url.pathname === "/__hmr" && is_dev) {
|
||||
response = await handleHmr({ req });
|
||||
if (url.pathname === "/__hmr" && is_dev) {
|
||||
return handleHmr({ req });
|
||||
}
|
||||
else if (url.pathname.startsWith("/.bunext")) {
|
||||
response = await handleBunextPublicAssets({ req });
|
||||
}
|
||||
else if (url.pathname.startsWith("/api/")) {
|
||||
response = await handleRoutes({ req });
|
||||
@@ -53,8 +67,12 @@ export default async function bunextRequestHandler({ req: initial_req, }) {
|
||||
return response;
|
||||
}
|
||||
catch (error) {
|
||||
return new Response(`Server Error: ${error.message}`, {
|
||||
status: 500,
|
||||
});
|
||||
if (is_dev) {
|
||||
return new Response(`Server Error: ${error.message}`, {
|
||||
status: 500,
|
||||
});
|
||||
}
|
||||
console.error(`Server Error: ${error.message}`, error);
|
||||
return new Response("Internal Server Error", { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export default function chokadirWatcherEsbuildCTX(): Promise<void>;
|
||||
@@ -0,0 +1,133 @@
|
||||
import chokidar from "chokidar";
|
||||
import path from "path";
|
||||
import { existsSync } from "fs";
|
||||
import grabDirNames from "../../utils/grab-dir-names";
|
||||
import fullRebuild from "./full-rebuild";
|
||||
import { AppData } from "../../data/app-data";
|
||||
import checkExcludedPatterns from "../../utils/check-excluded-patterns";
|
||||
import pagesSSRBundler from "../bundler/pages-ssr-bundler";
|
||||
import { log } from "../../utils/log";
|
||||
const { ROOT_DIR, BUNX_BUNDLER_ERROR_EXIT_FILE } = grabDirNames();
|
||||
export default async function chokadirWatcherEsbuildCTX() {
|
||||
const watcher = chokidar.watch(ROOT_DIR, {
|
||||
ignored: [
|
||||
/(^|[\/\\])\../,
|
||||
/node_modules/,
|
||||
/public/,
|
||||
/\.bunext/,
|
||||
/\.git/,
|
||||
/dist/,
|
||||
/bun\.lockb/,
|
||||
(path) => path.endsWith(AppData["BunextTmpFileExt"]),
|
||||
],
|
||||
persistent: true,
|
||||
ignoreInitial: true,
|
||||
depth: 99,
|
||||
});
|
||||
const handleEvent = async (event, filePath) => {
|
||||
let owns_recompile = false;
|
||||
try {
|
||||
const filename = path.relative(ROOT_DIR, filePath);
|
||||
if (existsSync(BUNX_BUNDLER_ERROR_EXIT_FILE)) {
|
||||
await fullRebuild();
|
||||
return;
|
||||
}
|
||||
if (global.BUNEXT_BUNDLER_CTX_DISPOSED) {
|
||||
await fullRebuild({ msg: `Restarting Bundler ...` });
|
||||
}
|
||||
if (global.BUNEXT_SSR_BUNDLER_CTX_DISPOSED) {
|
||||
await pagesSSRBundler().catch((error) => {
|
||||
log.error(`SSR Bundler Error: ${error}`);
|
||||
});
|
||||
}
|
||||
if (filename.match(/\/styles$/) || filename === "styles") {
|
||||
owns_recompile = true;
|
||||
global.BUNEXT_RECOMPILING = true;
|
||||
await Bun.sleep(1000);
|
||||
await fullRebuild({
|
||||
msg: `Detected new \`styles\` directory. Rebuilding ...`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (filename.match(/bunext.config\.ts/)) {
|
||||
await fullRebuild({
|
||||
msg: `bunext.config.ts file changed. Rebuilding server ...`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const target_files_match = /\.(tsx?|jsx?|css)$/;
|
||||
if (event === "change") {
|
||||
if (filename.match(target_files_match)) {
|
||||
if (global.BUNEXT_RECOMPILING)
|
||||
return;
|
||||
owns_recompile = true;
|
||||
global.BUNEXT_RECOMPILING = true;
|
||||
if (filename.match(/.*\.server\.tsx?/)) {
|
||||
global.BUNEXT_IS_SERVER_COMPONENT = true;
|
||||
}
|
||||
if (global.BUNEXT_BUNDLER_CTX) {
|
||||
await global.BUNEXT_BUNDLER_CTX.rebuild();
|
||||
}
|
||||
if (filename.match(/(404|500)\.tsx?/)) {
|
||||
for (let i = global.BUNEXT_HMR_CONTROLLERS.length - 1; i >= 0; i--) {
|
||||
const controller = global.BUNEXT_HMR_CONTROLLERS[i];
|
||||
try {
|
||||
controller?.controller?.enqueue(`event: update\ndata: ${JSON.stringify({ reload: true })}\n\n`);
|
||||
}
|
||||
catch {
|
||||
global.BUNEXT_HMR_CONTROLLERS.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (["add", "unlink", "addDir", "unlinkDir"].includes(event)) {
|
||||
const is_file_of_interest = !!filename.match(target_files_match) ||
|
||||
event.includes("Dir");
|
||||
if (!is_file_of_interest)
|
||||
return;
|
||||
if (!filename.match(/^src\/pages\/|\.css$/) ||
|
||||
checkExcludedPatterns({ path: filename }) ||
|
||||
filename.includes(" ")) {
|
||||
return reloadWatcher();
|
||||
}
|
||||
if (global.BUNEXT_RECOMPILING)
|
||||
return;
|
||||
owns_recompile = true;
|
||||
const action = event.startsWith("add") ? "created" : "deleted";
|
||||
const type = filename.match(/\.css$/)
|
||||
? "Stylesheet"
|
||||
: event.includes("Dir")
|
||||
? "Directory"
|
||||
: filename.match(/\/pages\/api\//)
|
||||
? "API Route"
|
||||
: "Page";
|
||||
await fullRebuild({
|
||||
msg: `${type} ${action}: ${filename}. Rebuilding ...`,
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
log.error(`Watcher rebuild failed: ${error}`);
|
||||
}
|
||||
finally {
|
||||
if (owns_recompile) {
|
||||
global.BUNEXT_RECOMPILING = false;
|
||||
global.BUNEXT_IS_SERVER_COMPONENT = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
watcher
|
||||
.on("add", (path) => handleEvent("add", path))
|
||||
.on("change", (path) => handleEvent("change", path))
|
||||
.on("unlink", (path) => handleEvent("unlink", path))
|
||||
.on("addDir", (path) => handleEvent("addDir", path))
|
||||
.on("unlinkDir", (path) => handleEvent("unlinkDir", path));
|
||||
}
|
||||
function reloadWatcher() {
|
||||
if (global.BUNEXT_PAGES_SRC_WATCHER) {
|
||||
global.BUNEXT_PAGES_SRC_WATCHER.close();
|
||||
chokadirWatcherEsbuildCTX();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { BundlerCTXMap } from "../../types";
|
||||
type Params = {
|
||||
new_artifacts: BundlerCTXMap[];
|
||||
};
|
||||
export default function cleanupArtifacts({ new_artifacts }: Params): Promise<void>;
|
||||
export {};
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import { log } from "../../utils/log";
|
||||
import path from "path";
|
||||
import grabDirNames from "../../utils/grab-dir-names";
|
||||
import { existsSync, readdirSync, statSync, unlinkSync } from "fs";
|
||||
const { ROOT_DIR, HYDRATION_DST_DIR_MAP_JSON_FILE_NAME } = grabDirNames();
|
||||
export default async function cleanupArtifacts({ new_artifacts }) {
|
||||
try {
|
||||
for (let i = 0; i < new_artifacts.length; i++) {
|
||||
const new_artifact = new_artifacts[i];
|
||||
const artifact_public_dir = path.dirname(path.join(ROOT_DIR, new_artifact.path));
|
||||
const dir_content = readdirSync(artifact_public_dir);
|
||||
for (let d = 0; d < dir_content.length; d++) {
|
||||
const dir_or_file = dir_content[d];
|
||||
const full_path = path.join(artifact_public_dir, dir_or_file);
|
||||
const file_or_path_stats = statSync(full_path);
|
||||
if (file_or_path_stats.isDirectory() ||
|
||||
dir_or_file == HYDRATION_DST_DIR_MAP_JSON_FILE_NAME) {
|
||||
continue;
|
||||
}
|
||||
if (new_artifact.path.includes(dir_or_file) ||
|
||||
new_artifact.css_path?.includes(dir_or_file)) {
|
||||
continue;
|
||||
}
|
||||
if (existsSync(full_path)) {
|
||||
unlinkSync(full_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export default function clearRequireCache(modulePath: string): void;
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
export default function clearRequireCache(modulePath) {
|
||||
const resolved = require.resolve(modulePath);
|
||||
const mod = require.cache[resolved];
|
||||
if (mod) {
|
||||
mod.children?.forEach((child) => {
|
||||
clearRequireCache(child.id);
|
||||
});
|
||||
delete require.cache[resolved];
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
export default function fullRebuild(params?: {
|
||||
msg?: string;
|
||||
}): Promise<void>;
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import { log } from "../../utils/log";
|
||||
import allPagesESBuildContextBundler from "../bundler/all-pages-esbuild-context-bundler";
|
||||
import serverPostBuildFn from "./server-post-build-fn";
|
||||
import watcherEsbuildCTX from "./watcher-esbuild-ctx";
|
||||
export default async function fullRebuild(params) {
|
||||
try {
|
||||
const { msg } = params || {};
|
||||
global.BUNEXT_RECOMPILING = true;
|
||||
if (msg) {
|
||||
log.watch(msg);
|
||||
}
|
||||
global.BUNEXT_ROUTER.reload();
|
||||
try {
|
||||
await global.BUNEXT_BUNDLER_CTX?.dispose();
|
||||
global.BUNEXT_BUNDLER_CTX = undefined;
|
||||
await global.BUNEXT_SSR_BUNDLER_CTX?.dispose();
|
||||
global.BUNEXT_SSR_BUNDLER_CTX = undefined;
|
||||
}
|
||||
catch (error) { }
|
||||
await allPagesESBuildContextBundler({
|
||||
post_build_fn: async () => {
|
||||
await serverPostBuildFn();
|
||||
},
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
finally {
|
||||
global.BUNEXT_RECOMPILING = false;
|
||||
global.BUNEXT_IS_SERVER_COMPONENT = false;
|
||||
}
|
||||
if (global.BUNEXT_PAGES_SRC_WATCHER) {
|
||||
global.BUNEXT_PAGES_SRC_WATCHER.close();
|
||||
watcherEsbuildCTX();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user