Compare commits

...
123 Commits
Author SHA1 Message Date
tben 1bf1b651db Update dev server 2026-08-14 13:12:35 +01:00
tben 247a64c873 Update dev server 2026-08-14 12:59:20 +01:00
tben f590deb11b Update dev server 2026-08-14 09:58:12 +01:00
tben 3426c7b53b Update dev server 2026-08-14 09:57:22 +01:00
tben afd1af1827 Update dev server 2026-08-14 09:55:57 +01:00
tben 22d3dedab2 Update page server types 2026-08-14 06:49:12 +01:00
tben 8d12329f01 Add perpetual watcher reload in dev 2026-08-13 05:39:14 +01:00
tben 78e86b3999 Rename global variables 2026-08-03 12:01:40 +01:00
tben 87948340b0 Add public envs 2026-08-03 08:42:33 +01:00
tben 86ea86e7bd Updates 2026-08-01 05:25:47 +01:00
tben 596b9de047 Add request cookie to HMR reload pipeline 2026-07-31 07:21:52 +01:00
tben 9da1e16318 Bugfix: Fix dev function 2026-07-30 14:50:54 +01:00
tben 823c5bb1ca Remove bun imports from esbuild bundler 2026-07-30 11:52:08 +01:00
tben 88ead3b3d6 Remove all Loader.registry.delete lines 2026-07-29 14:05:12 +01:00
tben 45509deff8 Dev Server Refresh Bugfix. Loop happens after a file route is refactored to a folder route with index.ts 2026-07-29 13:55:23 +01:00
tben a19863b3e9 Updates 2026-07-20 20:54:59 +01:00
tben a9cd51d71c Bugfix: update stale HMR after fixed errors 2026-07-20 20:54:39 +01:00
tben e3a0f5fbeb Error handling bugfix in dev server 2026-07-20 20:41:30 +01:00
tben 1d0ac4aa80 Update start logic 2026-07-01 19:23:05 +01:00
tben 817beacc7a Update build step 2026-07-01 19:18:11 +01:00
tben 61a9d8d612 Bugfix in spawn file 2026-06-23 13:07:28 +01:00
tben cd9ac833dc Switch back to fs watcher 2026-04-21 16:25:57 +01:00
tben e2b8b95a4b Switch watcher to chokidar 2026-04-20 16:12:24 +01:00
tben c06cb73181 Update tests folder ingore pattern 2026-04-20 06:18:32 +01:00
tben f3bb972a20 Ignore test files/folders in bundling 2026-04-20 06:15:55 +01:00
tben 40a987b983 Update banner log 2026-04-19 18:57:55 +01:00
tben ceeb6fbdaf Security Fixes 2026-04-19 16:17:45 +01:00
tben 4f5445e3df Security fixes pass #2 2026-04-19 16:17:19 +01:00
tben b702e26bf6 Security fixes pass #2 2026-04-19 16:00:59 +01:00
tben 3b26292124 Security fixes pass #1 2026-04-19 14:59:20 +01:00
tben cb5126c947 Refactor error handling 2026-04-19 11:12:11 +01:00
tben f018b228a8 Update tests 2026-04-19 10:23:08 +01:00
tben 95fcee36b2 Fix server component stale caching. Add server components to SSR bundler 2026-04-19 09:04:55 +01:00
tben 41e28d7a3e Updates 2026-04-18 18:06:21 +01:00
tben b2e92e5792 Updates 2026-04-18 11:17:13 +01:00
tben 9a427412f3 Refactor start and dev commands. Start in child processes. Handle errors better 2026-04-17 08:47:36 +01:00
tben a5f25d522e Updates 2026-04-15 16:09:07 +01:00
tben 938411653d Updates 2026-04-14 20:32:36 +01:00
tben b597e1420e Updates 2026-04-14 20:24:08 +01:00
tben d6f0a7962e Updates 2026-04-14 15:50:21 +01:00
tben f0aae8a8fa Refactor SSR strategy. Redirect before bundling any page component. Additional bugfixes for dev environment 2026-04-12 16:20:16 +01:00
tben 84d490b189 Bugfix. Fix SSR pages filters. Remove dedicated API bundler, use SSR bundler for pages and api routes. 2026-04-12 06:48:10 +01:00
tben 532d0d6b56 Update excluded pages paths logic. Add HMR retries on errors. 2026-04-12 05:47:12 +01:00
tben e0c2ab5872 Update pages server function(s) 2026-04-11 16:29:53 +01:00
tben 35f7a6fc85 Persist Request accorss hydration refreshes 2026-04-11 12:52:56 +01:00
tben 814a289460 Fix API bundler. Use new esbuild context builder for it. 2026-04-11 12:06:53 +01:00
tben c4f7cf9164 Fix api route caching issue 2026-04-11 10:00:24 +01:00
tben af8c207ac1 Add retries to page component function error. Reload for all 404 pages 2026-04-11 08:48:01 +01:00
tben 5a0972beb8 Update API routes function pass #3 2026-04-10 11:54:17 +01:00
tben 257adfec39 Update API routes function pass #2 2026-04-10 11:48:30 +01:00
tben 349b99bacf Fix types error 2026-04-10 11:42:22 +01:00
tben 972f6945c2 Fix types error 2026-04-10 11:39:50 +01:00
tben 6477f446d1 Update API routes function 2026-04-10 11:34:22 +01:00
tben 7fb1784b95 Bugfix: Refactor SSR bundler. Fix stale cache SSR modules. 2026-04-09 23:50:58 +01:00
tben 40fc7778a8 Bugfix: SSR bundler errors fixed. Caching still faulty 2026-04-09 22:33:49 +01:00
tben f3b087a1f3 Handle errors better 2026-04-09 18:01:29 +01:00
tben eb0721f94b Updates 2026-04-09 07:47:38 +01:00
tben ab6fc3be26 Hydration bugfix 2026-04-08 08:08:27 +01:00
tben 6b7d29bc53 Bugfix: hydration mismatch 2026-04-08 07:46:09 +01:00
tben a84ac10b24 Bugfix. Update watcher to reload on excluded file/directory addition and deletion. 2026-04-08 06:51:44 +01:00
tben 4b3a4dbc77 Bugfix. Extract react imports from main bundler to vendor static files 2026-04-08 05:39:11 +01:00
tben c19b7c9607 Fix react and react-dom peer dependencies mismatch #4 2026-04-05 18:25:14 +01:00
tben 57957b89a4 Update README.md 2026-04-05 18:13:52 +01:00
tben d441f9982e Fix react and react-dom peer dependencies mismatch #3 2026-04-05 17:58:52 +01:00
tben f49a17b24e Fix react and react-dom peer dependencies #2 2026-04-05 17:13:25 +01:00
tben fa8a108686 Fix react and react-dom peer dependencies 2026-04-05 17:09:22 +01:00
tben 8d5b2a5c07 Update dev debug logs 2026-04-05 06:34:28 +01:00
tben 333c84281d Update watcher to rebuild on api dir change 2026-04-05 06:09:22 +01:00
tben 5166ba037f Component Bundler Bugfix 2026-04-04 20:30:40 +01:00
tben 1037bbb546 Component Bundler Bugfix debug 2026-04-04 20:05:41 +01:00
tben f34f8baa99 Update watcher and component gen functions 2026-04-04 20:02:36 +01:00
tben 48c25d2fb4 Update init function 2026-04-04 19:35:14 +01:00
tben 7dbd1f7e12 Remove esm.sh externals 2026-03-31 06:57:44 +01:00
tben 68d88bce4a Try local react imports 2026-03-31 06:22:26 +01:00
tben d6985b3335 Update meta logic 2026-03-30 20:40:15 +01:00
tben 86d4bb8d6c Imporve HMR Post-build function speed 2026-03-30 05:44:34 +01:00
tben 2b170d24e1 Updates 2026-03-29 17:08:10 +01:00
tben 950bdc3dca Add error logs for server functions. Add cache dynamic function. 2026-03-27 05:22:31 +01:00
tben 572f739b5c Surpress react/jsx-runtime errors: Bugfix #3 2026-03-25 16:35:45 +01:00
tben ee07163ba5 Surpress react/jsx-runtime errors: Bugfix #2 2026-03-25 16:26:05 +01:00
tben ad51f7da0c Surpress react/jsx-runtime errors: Bugfix #2 2026-03-25 16:25:39 +01:00
tben 6a024faece Surpress react/jsx-runtime errors: Bugfix 2026-03-25 16:09:18 +01:00
tben 764e8f76f0 Surpress react/jsx-runtime errors 2026-03-25 16:03:45 +01:00
tben b8bae51b32 Remove react/jsx-runtime dynamic browser import 2026-03-25 15:51:24 +01:00
tben dad5cffaac Remove react/jsx-dev-runtime dynamic browser import 2026-03-25 15:48:05 +01:00
tben 321c8ebb89 Refactor Bundler. Fix bugs. 2026-03-24 22:03:55 +01:00
tben 336fa812a5 Update bundler logic 2026-03-24 21:37:41 +01:00
tben f6c7f6b78c Refactor bundler function 2026-03-24 20:42:47 +01:00
tben 99b319f5af Bugfix. Fix esbuild post build function for HMR 2026-03-24 18:41:33 +01:00
tben 47c262392c Switch back to esbuild 2026-03-24 18:28:56 +01:00
tben 976ff5fec9 Reconcile React and React Dom Versions 2026-03-24 07:34:55 +01:00
tben 60ee353bf0 Refactor Server Paradigm. 2026-03-24 07:18:57 +01:00
tben 342f56f3f5 Server Refactor 2026-03-24 07:17:03 +01:00
tben 7dd8d87be8 Change server paradigm 2026-03-24 05:24:14 +01:00
tben 6f1db7c01f Add more features. Customize HTML. Add server logic and other page module exports to __root 2026-03-23 19:14:56 +01:00
tben 67ed8749e2 Remove React and React Dom deletion 2026-03-23 07:50:55 +01:00
tben 146f51c590 Remove React and React Dom enforcment 2026-03-23 07:46:10 +01:00
tben 4b8b610e32 Updates 2026-03-23 07:42:00 +01:00
tben 567fb4f746 Fix errors, bugs and known vulerabilities 2026-03-23 06:58:41 +01:00
tben 0308ea32ec Update Hydration and HMR logic 2026-03-23 06:13:25 +01:00
tben 1634eeb213 Major Refactoring. Test update 2026-03-22 16:45:12 +01:00
tben e047539a11 Remove unneccesary logs 2026-03-22 12:44:11 +01:00
tben e0a07cf57c Remove unneccesary logs 2026-03-22 12:38:04 +01:00
tben e2df3256f4 Bogfix: update server module stripping function. 2026-03-22 12:32:15 +01:00
tben c3e1341ff8 Update HMR. Make page reload when __root.tsx file is changed. 2026-03-22 12:09:24 +01:00
tben 49d6781170 Update web-page handler: add more detailed error logs 2026-03-22 10:56:48 +01:00
tben 3d044ee79e Bugfix. Update Tsx server stripping function to have absolute paths. Run the rewrite function on dev and build commands. 2026-03-22 10:53:27 +01:00
tben a9af20a8b2 Major Bugfix. Fix server component client compatibility 2026-03-22 10:34:30 +01:00
tben 9f619c8898 Update init. Remove uneccessary directores creation 2026-03-22 05:06:36 +01:00
tben 32056b5cb6 Update HMR logic. Refresh on server props update. 2026-03-21 18:03:36 +01:00
tben a504046a36 Update dependencies. 2026-03-21 17:23:20 +01:00
tben 5ebac74353 Updates 2026-03-21 17:16:33 +01:00
tben 070e6860d8 Updates 2026-03-21 17:08:04 +01:00
tben d75125d9fa Add Setup for git.tben.me. Add publish.sh script 2026-03-21 16:47:49 +01:00
tben 4ee3876710 Update bundler. Handle non-existent file error. 2026-03-21 16:35:30 +01:00
tben 632c70fc90 Update watcher function. Add .css files to the rebuild pipeline 2026-03-21 14:58:41 +01:00
tben 13bd8bb851 Update README.md 2026-03-21 14:36:32 +01:00
tben 4b79993d37 Bugfix. Update generate-web-html function 2026-03-21 14:24:12 +01:00
tben 51db0c447c Bugfix. Update generate-web-html function 2026-03-21 14:05:02 +01:00
tben a38841a587 Major Bugfix. Update init function 2026-03-21 13:52:53 +01:00
tben d3336b48c7 Bugfixes: update types, update imports, update init function. 2026-03-21 13:47:40 +01:00
tben 3545b6dc08 Update tests 2026-03-21 10:20:16 +01:00
tben d2ddaef0d4 Update tests 2026-03-21 10:14:38 +01:00
304 changed files with 9840 additions and 3308 deletions
+8 -1
View File
@@ -175,4 +175,11 @@ out
/test /test
.bunext .bunext
/bin /bin
/build /build
__fixtures__
/public
/.data
/.dump
/.vscode
/source.md
SECURITY.md
+2
View File
@@ -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}
+14 -4
View File
@@ -40,11 +40,21 @@ bunext start # Start production server from pre-built artifacts
- `src/presets/` — Default 404/500 components and sample `bunext.config.ts` - `src/presets/` — Default 404/500 components and sample `bunext.config.ts`
### Page module contract ### Page module contract
Pages live in `src/pages/`. A page file may export: 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.
- Default export: React component receiving `ServerProps | StaticProps`
- `server`: `BunextPageServerFn`runs server-side before rendering, return value becomes props **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 - `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. API routes live in `src/pages/api/` and follow standard Bun `Request → Response` handler conventions.
+267 -129
View File
File diff suppressed because it is too large Load Diff
+61 -23
View File
@@ -1,12 +1,18 @@
{ {
"lockfileVersion": 1, "lockfileVersion": 1,
"configVersion": 0,
"workspaces": { "workspaces": {
"": { "": {
"name": "bun-next", "name": "bun-next",
"dependencies": { "dependencies": {
"@tailwindcss/postcss": "^4.2.2", "@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", "bun-plugin-tailwind": "^0.1.2",
"chalk": "^5.6.2", "chalk": "^5.6.2",
"chokidar": "^5.0.0",
"commander": "^14.0.2", "commander": "^14.0.2",
"esbuild": "^0.27.4", "esbuild": "^0.27.4",
"lightningcss-wasm": "^1.32.0", "lightningcss-wasm": "^1.32.0",
@@ -14,27 +20,29 @@
"micromatch": "^4.0.8", "micromatch": "^4.0.8",
"ora": "^9.0.0", "ora": "^9.0.0",
"postcss": "^8.5.8", "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": "^19.2.4",
"react-dom": "^19.2.4", "react-dom": "^19.2.4",
}, "tailwindcss": "^4.2.2",
"peerDependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0",
"typescript": "^5.0.0", "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": { "packages": {
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], "@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/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=="], "@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=="], "@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/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=="], "@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/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=="], "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-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=="], "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-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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="],
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], "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=="], "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": ["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=="], "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=="], "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=="], "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=="], "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=="], "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": ["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-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=="], "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=="], "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=="], "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=="], "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=="], "@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=="], "@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=="], "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=="], "strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
"bun-types/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
} }
} }
+28 -12
View File
@@ -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) | | Router | `Bun.FileSystemRouter` | Custom (Pages Router) / React Router (App Router) |
| SSR method | `renderToString` (complete response, by design) | `renderToReadableStream` (streaming) | | SSR method | `renderToString` (complete response, by design) | `renderToReadableStream` (streaming) |
| Component model | Classic SSR + hydration | React Server Components + Client Components | | 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 | | State persistence | `window.__PAGE_PROPS__` | RSC payload, router cache |
| Dev HMR transport | Server-Sent Events (SSE) | WebSocket | | Dev HMR transport | Server-Sent Events (SSE) | WebSocket |
| Config format | `bunext.config.ts` | `next.config.js` / `next.config.ts` | | 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 ### 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 ```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(...); const data = await db.query(...);
return { props: { data } }; 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:** **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. - 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. - 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**: **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 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`. - 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. - 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 ### 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: 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. - `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 ### 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 ```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); const user = await getUser(ctx.req);
return { return {
props: { data }, props: { data },
@@ -551,16 +560,21 @@ export const server: BunextPageServerFn = async (ctx) => {
cacheExpiry: 300, 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. 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 ### 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 ```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) => { ctx.resTransform = (res) => {
res.headers.set("X-Custom-Header", "value"); res.headers.set("X-Custom-Header", "value");
return res; return res;
@@ -568,6 +582,8 @@ export const server: BunextPageServerFn = async (ctx) => {
return { props: {} }; 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. 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. 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 ### No Vendor Lock-In
-37
View File
@@ -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
View File
@@ -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({});
});
});
-1
View File
@@ -1 +0,0 @@
export {};
-52
View File
@@ -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
View File
@@ -1 +0,0 @@
export {};
-23
View File
@@ -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
View File
@@ -1 +0,0 @@
export {};
-38
View File
@@ -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
View File
@@ -1 +0,0 @@
export {};
-30
View File
@@ -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
View File
@@ -1 +0,0 @@
export {};
-55
View File
@@ -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
View File
@@ -1 +0,0 @@
export {};
-32
View File
@@ -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
View File
@@ -1 +0,0 @@
export {};
-46
View File
@@ -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
View File
@@ -1 +0,0 @@
export {};
-32
View File
@@ -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
View File
@@ -1 +0,0 @@
export {};
-43
View File
@@ -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);
});
});
-2
View File
@@ -1,2 +0,0 @@
#!/usr/bin/env bun
export {};
-131
View File
@@ -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`);
+13 -10
View File
@@ -1,18 +1,21 @@
import { Command } from "commander"; import { Command } from "commander";
import allPagesBundler from "../../functions/bundler/all-pages-bundler";
import { log } from "../../utils/log"; 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 () { export default function () {
return new Command("build") return new Command("build")
.description("Build Project") .description("Build Project")
.action(async () => { .action(async () => {
process.env.NODE_ENV = "production"; try {
process.env.BUILD = "true"; rmSync(HYDRATION_DST_DIR, { recursive: true });
await init(); rmSync(BUNX_CWD_PAGES_REWRITE_DIR, { recursive: true });
log.banner(); }
log.build("Building Project ..."); catch (error) { }
allPagesBundler({ global.BUNEXT_SKIPPED_BROWSER_MODULES = new Set();
exit_after_first_build: true, await bunextInit({ build_only: true });
}); log.success("Modules Built Successfully!");
process.exit();
}); });
} }
+25
View File
@@ -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}`);
}
+54 -7
View File
@@ -1,14 +1,61 @@
import { Command } from "commander"; import { Command } from "commander";
import startServer from "../../functions/server/start-server"; import path from "path";
import { log } from "../../utils/log"; import grabDirNames from "../../utils/grab-dir-names";
import bunextInit from "../../functions/bunext-init"; import writeErrorFile from "../../functions/write-error-file";
import { existsSync } from "fs";
let retries = 0;
let timeout;
const MAX_RETRIES = 5;
export default function () { export default function () {
return new Command("dev") return new Command("dev")
.description("Run development server") .description("Run development server")
.action(async () => { .action(async () => {
process.env.NODE_ENV == "development"; await dev();
log.info("Running development server ...");
await bunextInit();
await startServer();
}); });
} }
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);
}
+5 -2
View File
@@ -3,13 +3,14 @@ import { program } from "commander";
import start from "./start"; import start from "./start";
import dev from "./dev"; import dev from "./dev";
import build from "./build"; import build from "./build";
import { log } from "../utils/log";
/** /**
* # Describe Program * # Describe Program
*/ */
program program
.name(`bunext`) .name(`bunext`)
.description(`A React Next JS replacement built with bun JS`) .description(`A React Next JS replacement built with bun JS`)
.version(`1.0.0`); .version(`1.0.43`);
/** /**
* # Declare Commands * # Declare Commands
*/ */
@@ -20,7 +21,9 @@ program.addCommand(build());
* # Handle Unavailable Commands * # Handle Unavailable Commands
*/ */
program.on("command:*", () => { 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); process.exit(1);
}); });
/** /**
+53 -7
View File
@@ -1,14 +1,60 @@
import { Command } from "commander"; import { Command } from "commander";
import startServer from "../../functions/server/start-server"; import path from "path";
import { log } from "../../utils/log"; import writeErrorFile from "../../functions/write-error-file";
import bunextInit from "../../functions/bunext-init"; import { existsSync } from "fs";
let retries = 0;
let timeout;
const MAX_RETRIES = 5;
export default function () { export default function () {
return new Command("start") return new Command("start")
.description("Start production server") .description("Start production server")
.action(async () => { .action(async () => {
process.env.NODE_ENV = "production"; await start();
log.info("Starting production server ...");
await bunextInit();
await startServer();
}); });
} }
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);
}
+6
View File
@@ -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();
+3
View File
@@ -4,4 +4,7 @@ export declare const AppData: {
readonly BunextStaticFilesCacheExpiry: number; readonly BunextStaticFilesCacheExpiry: number;
readonly ClientHMRPath: "__bunext_client_hmr__"; readonly ClientHMRPath: "__bunext_client_hmr__";
readonly BunextClientHydrationScriptID: "bunext-client-hydration-script"; readonly BunextClientHydrationScriptID: "bunext-client-hydration-script";
readonly BunextTmpFileExt: ".bunext_tmp.tsx";
readonly BunextHMRRetryRoute: "/.bunext/hmr-retry";
readonly DefaultMaxLogs: 50;
}; };
+3
View File
@@ -4,4 +4,7 @@ export const AppData = {
BunextStaticFilesCacheExpiry: 60 * 60 * 24 * 7, BunextStaticFilesCacheExpiry: 60 * 60 * 24 * 7,
ClientHMRPath: "__bunext_client_hmr__", ClientHMRPath: "__bunext_client_hmr__",
BunextClientHydrationScriptID: "bunext-client-hydration-script", BunextClientHydrationScriptID: "bunext-client-hydration-script",
BunextTmpFileExt: ".bunext_tmp.tsx",
BunextHMRRetryRoute: "/.bunext/hmr-retry",
DefaultMaxLogs: 50,
}; };
-10
View File
@@ -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
View File
@@ -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,
};
});
},
};
}
+1
View File
@@ -0,0 +1 @@
export default function apiRoutesBundler(): Promise<void>;
+40
View File
@@ -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>;
+44
View File
@@ -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 {};
+18
View File
@@ -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;
}
+1
View File
@@ -0,0 +1 @@
export default function bunReactModulesBundler(): Promise<void>;
+74
View File
@@ -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"; import type { BundlerCTXMap, PageFiles } from "../../types";
type Params = { type Params = {
result: esbuild.BuildResult<esbuild.BuildOptions>; 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 {}; export {};
@@ -1,19 +1,25 @@
import path from "path"; import path from "path";
import * as esbuild from "esbuild"; 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) if (result.errors.length > 0)
return; return;
const virtual_regex = new RegExp(`^${virtual_match}:`);
const artifacts = Object.entries(result.metafile.outputs) const artifacts = Object.entries(result.metafile.outputs)
.filter(([, meta]) => meta.entryPoint) .filter(([, meta]) => meta.entryPoint)
.map(([outputPath, meta]) => { .map(([outputPath, meta]) => {
const target_page = pages.find((p) => { const entrypoint = meta.entryPoint?.match(virtual_regex)
return meta.entryPoint === `virtual:${p.local_path}`; ? meta.entryPoint?.replace(virtual_regex, "")
}); : meta.entryPoint
? path.join(ROOT_DIR, meta.entryPoint)
: "";
const target_page = entryToPage.get(entrypoint);
if (!target_page || !meta.entryPoint) { if (!target_page || !meta.entryPoint) {
return undefined; return undefined;
} }
const { file_name, local_path, url_path } = target_page; const { file_name, local_path, url_path } = target_page;
const cssPath = meta.cssBundle || undefined;
return { return {
path: outputPath, path: outputPath,
hash: path.basename(outputPath, path.extname(outputPath)), hash: path.basename(outputPath, path.extname(outputPath)),
@@ -21,7 +27,7 @@ export default function grabArtifactsFromBundledResults({ result, pages, }) {
? "text/css" ? "text/css"
: "text/javascript", : "text/javascript",
entrypoint: meta.entryPoint, entrypoint: meta.entryPoint,
css_path: cssPath, css_path: meta.cssBundle,
file_name, file_name,
local_path, local_path,
url_path, url_path,
+1 -1
View File
@@ -1,5 +1,5 @@
type Params = { type Params = {
page_local_path: string; 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 {}; export {};
+34 -15
View File
@@ -1,25 +1,44 @@
import { existsSync } from "fs"; import { existsSync } from "fs";
import path from "path";
import grabDirNames from "../../utils/grab-dir-names"; import grabDirNames from "../../utils/grab-dir-names";
import AppNames from "../../utils/grab-app-names";
import grabConstants from "../../utils/grab-constants"; import grabConstants from "../../utils/grab-constants";
const { PAGES_DIR } = grabDirNames(); import grabRootFilePath from "../server/web-pages/grab-root-file-path";
export default function grabClientHydrationScript({ page_local_path }) { const { ROOT_DIR } = grabDirNames();
const { ClientRootElementIDName, ClientRootComponentWindowName } = grabConstants(); export default async function grabClientHydrationScript({ page_local_path, }) {
const root_component_path = path.join(PAGES_DIR, `${AppNames["RootPagesComponentName"]}.tsx`); const { ClientRootElementIDName, ClientRootComponentWindowName, ClientWindowPagePropsName, } = grabConstants();
const does_root_exist = existsSync(root_component_path); 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 = ``; let txt = ``;
txt += `import { hydrateRoot, createElement } from "react-dom/client";\n`; txt += `import { hydrateRoot } from "${ROOT_DIR}/node_modules/react-dom/client.js";\n`;
if (does_root_exist) { // txt += `import react from "${ROOT_DIR}/node_modules/react/index.js";\n`;
txt += `import Root from "${root_component_path}";\n`; if (root_file_path) {
txt += `import Root from "${root_file_path}";\n`;
} }
txt += `import Page from "${page_local_path}";\n\n`; txt += `import Page from "${page_local_path}";\n\n`;
txt += `const pageProps = window.__PAGE_PROPS__ || {};\n`; txt += `const pageProps = window.${ClientWindowPagePropsName} || {};\n`;
if (does_root_exist) { if (root_file_path) {
txt += `const component = <Root suppressHydrationWarning={true} {...pageProps}><Page {...pageProps} /></Root>\n`; txt += `const component = <Root {...pageProps}><Page {...pageProps} /></Root>\n`;
} }
else { else {
txt += `const component = <Page suppressHydrationWarning={true} {...pageProps} />\n`; txt += `const component = <Page {...pageProps} />\n`;
} }
txt += `if (window.${ClientRootComponentWindowName}?.render) {\n`; txt += `if (window.${ClientRootComponentWindowName}?.render) {\n`;
txt += ` window.${ClientRootComponentWindowName}.render(component);\n`; txt += ` window.${ClientRootComponentWindowName}.render(component);\n`;
@@ -29,7 +48,7 @@ export default function grabClientHydrationScript({ page_local_path }) {
txt += ` } });\n\n`; txt += ` } });\n\n`;
txt += ` window.${ClientRootComponentWindowName} = root;\n`; txt += ` window.${ClientRootComponentWindowName} = root;\n`;
txt += ` window.__BUNEXT_RERENDER__ = (NewPage) => {\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 += ` root.render(<NewPage {...props} />);\n`;
txt += ` };\n`; txt += ` };\n`;
txt += `}\n`; txt += `}\n`;
+7
View File
@@ -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
View File
@@ -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}`);
}
}
+7
View File
@@ -0,0 +1,7 @@
type Params = {
post_build_fn?: (params: {
artifacts: any[];
}) => Promise<void> | void;
};
export default function pagesSSRContextBundler(params?: Params): Promise<void>;
export {};
+69
View File
@@ -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;
+2
View File
@@ -0,0 +1,2 @@
import * as esbuild from "esbuild";
export default function reactVendorChunkPlugin(): esbuild.Plugin;
+97
View File
@@ -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;
}
+3
View File
@@ -0,0 +1,3 @@
import type { Plugin } from "esbuild";
declare const reactAliasPlugin: Plugin;
export default reactAliasPlugin;
+22
View File
@@ -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 {};
+39
View File
@@ -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;
}
+1
View File
@@ -0,0 +1 @@
export default function reactModulesBundler(): Promise<void>;
+77
View File
@@ -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`,
},
};
}
+7
View File
@@ -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
View File
@@ -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),
// );
}
+6
View File
@@ -0,0 +1,6 @@
type Params = {
txt_code: string;
file_path: string;
};
export default function stripServerSideLogic({ txt_code, file_path }: Params): string;
export {};
+92
View File
@@ -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]);
}
+46 -18
View File
@@ -1,26 +1,54 @@
import { type Ora } from "ora";
import type { BundlerCTXMap, BunextConfig, GlobalHMRControllerObject, PageFiles } from "../types"; import type { BundlerCTXMap, BunextConfig, GlobalHMRControllerObject, PageFiles } from "../types";
import type { FileSystemRouter, Server } from "bun"; import type { FileSystemRouter, Server } from "bun";
import { type DirNames } from "../utils/grab-dir-names";
import type { BuildContext } from "esbuild"; 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 Variables
*/ */
declare global { declare global {
var ORA_SPINNER: Ora; var BUNEXT_CONFIG: BunextConfig;
var CONFIG: BunextConfig; var BUNEXT_SERVER: Server<any> | undefined;
var SERVER: Server | undefined; var BUNEXT_RECOMPILING: boolean;
var RECOMPILING: boolean; var BUNEXT_BUILDING_SSR: boolean;
var WATCHER_TIMEOUT: any; var BUNEXT_IS_SERVER_COMPONENT: boolean;
var ROUTER: FileSystemRouter; var BUNEXT_WATCHER_TIMEOUT: any;
var HMR_CONTROLLERS: GlobalHMRControllerObject[]; var BUNEXT_ROUTER: FileSystemRouter;
var LAST_BUILD_TIME: number; var BUNEXT_HMR_CONTROLLERS: GlobalHMRControllerObject[];
var BUNDLER_CTX: BuildContext | undefined; var BUNEXT_LAST_BUILD_TIME: number;
var BUNDLER_CTX_MAP: BundlerCTXMap[] | undefined; var BUNEXT_BUNDLER_CTX_MAP: {
var IS_FIRST_BUNDLE_READY: boolean; [k: string]: BundlerCTXMap;
var BUNDLER_REBUILDS: 0; };
var PAGES_SRC_WATCHER: FSWatcher | undefined; var BUNEXT_SSR_BUNDLER_CTX_MAP: {
var CURRENT_VERSION: string | undefined; [k: string]: BundlerCTXMap;
var PAGE_FILES: PageFiles[]; };
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 {};
+43 -44
View File
@@ -1,61 +1,60 @@
import ora, {} from "ora"; import grabDirNames, {} from "../utils/grab-dir-names";
import grabDirNames from "../utils/grab-dir-names";
import { readFileSync } from "fs";
import init from "./init"; import init from "./init";
import isDevelopment from "../utils/is-development"; 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 { log } from "../utils/log";
import cron from "./server/cron"; import cron from "./server/cron";
export default async function bunextInit() { import allPagesESBuildContextBundler from "./bundler/all-pages-esbuild-context-bundler";
global.ORA_SPINNER = ora(); import serverPostBuildFn from "./server/server-post-build-fn";
global.ORA_SPINNER.clear(); import reactModulesBundler from "./bundler/react-modules-bundler";
global.HMR_CONTROLLERS = []; import grabConstants from "../utils/grab-constants";
global.IS_FIRST_BUNDLE_READY = false; import watcherEsbuildCTX from "./server/watcher-esbuild-ctx";
global.BUNDLER_REBUILDS = 0; const dirNames = grabDirNames();
global.PAGE_FILES = []; 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(); await init();
log.banner(); log.banner();
const { PAGES_DIR, HYDRATION_DST_DIR_MAP_JSON_FILE } = grabDirNames(); global.BUNEXT_CONSTANTS = grabConstants();
await reactModulesBundler();
const router = new Bun.FileSystemRouter({ const router = new Bun.FileSystemRouter({
style: "nextjs", style: "nextjs",
dir: PAGES_DIR, dir: PAGES_DIR,
}); });
global.ROUTER = router; global.BUNEXT_ROUTER = router;
const is_dev = isDevelopment(); const is_dev = isDevelopment();
if (is_dev) { if (params?.build_only) {
await allPagesBundler({ log.build(`Building Modules ...`);
watch: true, await allPagesESBuildContextBundler();
post_build_fn: serverPostBuildFn, }
else if (is_dev) {
log.build(`Building Modules ...`);
await allPagesESBuildContextBundler({
post_build_fn: async () => {
await serverPostBuildFn();
},
}); });
watcher(); watcherEsbuildCTX();
} }
else { else {
const artifacts = EJSON.parse(readFileSync(HYDRATION_DST_DIR_MAP_JSON_FILE, "utf-8")); log.build(`Building Modules ...`);
if (!artifacts?.[0]) { await allPagesESBuildContextBundler({ start: true });
log.error("Please build first.");
process.exit(1);
}
global.BUNDLER_CTX_MAP = artifacts;
global.IS_FIRST_BUNDLE_READY = true;
cron(); 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
View File
@@ -13,6 +13,10 @@ export default async function trimAllCache() {
const trim_key = await trimCacheKey({ const trim_key = await trimCacheKey({
key: cache_key, key: cache_key,
}); });
if (trim_key.success) {
cached_items.splice(i, 1);
i--;
}
} }
} }
catch (error) { catch (error) {
+2 -2
View File
@@ -9,8 +9,8 @@ export default async function trimCacheKey({ key, }) {
const { cache_name, cache_meta_name } = grabCacheNames({ const { cache_name, cache_meta_name } = grabCacheNames({
key, key,
}); });
const config = global.CONFIG; const config = global.BUNEXT_CONFIG;
const default_expiry_time_seconds = config.defaultCacheExpiry || const default_expiry_time_seconds = config.default_cache_expiry ||
AppData["DefaultCacheExpiryTimeSeconds"]; AppData["DefaultCacheExpiryTimeSeconds"];
const default_expiry_time_milliseconds = default_expiry_time_seconds * 1000; const default_expiry_time_milliseconds = default_expiry_time_seconds * 1000;
const cache_content_path = path.join(BUNEXT_CACHE_DIR, cache_name); const cache_content_path = path.join(BUNEXT_CACHE_DIR, cache_name);
+1
View File
@@ -0,0 +1 @@
export default function cleanupLogsDirs(): void;
+48
View File
@@ -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();
+23 -7
View File
@@ -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 grabDirNames from "../utils/grab-dir-names";
import { execSync } from "child_process";
import path from "path"; import path from "path";
import grabConfig from "./grab-config"; import grabConfig from "./grab-config";
import { log } from "../utils/log";
export default async function () { export default async function () {
const dirNames = grabDirNames(); const dirNames = grabDirNames();
execSync(`rm -rf ${dirNames.BUNEXT_CACHE_DIR}`); const is_dev = !Boolean(process.env.NODE_ENV == "production");
execSync(`rm -rf ${dirNames.BUNX_CWD_MODULE_CACHE_DIR}`); 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 { try {
const package_json = await Bun.file(path.resolve(__dirname, "../../package.json")).json(); const package_json = await Bun.file(path.resolve(__dirname, "../../package.json")).json();
const current_version = package_json.version; const current_version = package_json.version;
global.CURRENT_VERSION = current_version; global.BUNEXT_CURRENT_VERSION = current_version;
} }
catch (error) { } catch (error) { }
const keys = Object.keys(dirNames); const keys = Object.keys(dirNames);
for (let i = 0; i < keys.length; i++) { for (let i = 0; i < keys.length; i++) {
const key = keys[i]; const key = keys[i];
const dir = dirNames[key]; const dir = dirNames[key];
if (!dir.match(/\//))
continue;
if (!existsSync(dir) && !dir.match(/\.\w+$/)) { if (!existsSync(dir) && !dir.match(/\.\w+$/)) {
mkdirSync(dir, { recursive: true }); mkdirSync(dir, { recursive: true });
continue; continue;
@@ -29,8 +45,8 @@ export default async function () {
} }
} }
const config = (await grabConfig()) || {}; const config = (await grabConfig()) || {};
global.CONFIG = { global.BUNEXT_CONFIG = {
...config, ...config,
development: true, development: is_dev,
}; };
} }
+46
View File
@@ -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
View File
@@ -1,5 +1,6 @@
type Params = { type Params = {
req: Request; 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 {}; export {};
+32 -14
View File
@@ -1,21 +1,26 @@
import handleWebPages from "./web-pages/handle-web-pages"; import handleWebPages from "./web-pages/handle-web-pages";
import handleRoutes from "./handle-routes"; import handleRoutes from "./handle-routes";
import isDevelopment from "../../utils/is-development"; import isDevelopment from "../../utils/is-development";
import grabConstants from "../../utils/grab-constants";
import { AppData } from "../../data/app-data";
import handleHmr from "./handle-hmr"; import handleHmr from "./handle-hmr";
import handleHmrUpdate from "./handle-hmr-update";
import handlePublic from "./handle-public"; import handlePublic from "./handle-public";
import handleFiles from "./handle-files"; 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(); const is_dev = isDevelopment();
let req = initial_req.clone(); let req = initial_req.clone();
try { try {
const url = new URL(req.url); 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; let response = undefined;
if (config?.middleware) { if (global.BUNEXT_CONSTANTS.config?.middleware) {
const middleware_res = await config.middleware({ const middleware_res = await global.BUNEXT_CONSTANTS.config.middleware({
req: initial_req, req: initial_req,
url, url,
}); });
@@ -26,11 +31,20 @@ export default async function bunextRequestHandler({ req: initial_req, }) {
req = middleware_res; req = middleware_res;
} }
} }
if (url.pathname == `/${AppData["ClientHMRPath"]}`) { if (is_dev && url.pathname == AppData["BunextHMRRetryRoute"]) {
response = await handleHmrUpdate({ req }); 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) { if (url.pathname === "/__hmr" && is_dev) {
response = await handleHmr({ req }); return handleHmr({ req });
}
else if (url.pathname.startsWith("/.bunext")) {
response = await handleBunextPublicAssets({ req });
} }
else if (url.pathname.startsWith("/api/")) { else if (url.pathname.startsWith("/api/")) {
response = await handleRoutes({ req }); response = await handleRoutes({ req });
@@ -53,8 +67,12 @@ export default async function bunextRequestHandler({ req: initial_req, }) {
return response; return response;
} }
catch (error) { catch (error) {
return new Response(`Server Error: ${error.message}`, { if (is_dev) {
status: 500, 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>;
+133
View File
@@ -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();
}
}
+6
View File
@@ -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
View File
@@ -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);
}
}
+1
View File
@@ -0,0 +1 @@
export default function clearRequireCache(modulePath: string): void;
+10
View File
@@ -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
View File
@@ -0,0 +1,3 @@
export default function fullRebuild(params?: {
msg?: string;
}): Promise<void>;
+37
View File
@@ -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