Compare commits

...
30 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
121 changed files with 1964 additions and 996 deletions
+1
View File
@@ -182,3 +182,4 @@ __fixtures__
/.dump /.dump
/.vscode /.vscode
/source.md /source.md
SECURITY.md
+31 -1
View File
@@ -742,6 +742,9 @@ const config: BunextConfig = {
globalVars: { globalVars: {
MY_API_URL: "https://api.example.com", MY_API_URL: "https://api.example.com",
}, },
public_envs: {
BUNEXT_PUBLIC_APP_NAME: "My App",
},
development: false, // forced by the CLI; set manually if needed development: false, // forced by the CLI; set manually if needed
}; };
@@ -755,6 +758,7 @@ export default config;
| `distDir` | `string` | `.bunext` | Internal artifact directory | | `distDir` | `string` | `.bunext` | Internal artifact directory |
| `assetsPrefix` | `string` | `_bunext/static` | URL prefix for static assets | | `assetsPrefix` | `string` | `_bunext/static` | URL prefix for static assets |
| `globalVars` | `{ [k: string]: any }` | — | Variables injected globally at build time | | `globalVars` | `{ [k: string]: any }` | — | Variables injected globally at build time |
| `public_envs` | `Record<string, string>` | — | Public env vars exposed to the client via `window.process.env` (see [Environment Variables](#environment-variables)) |
| `development` | `boolean` | — | Overridden to `true` by `bunext dev` automatically | | `development` | `boolean` | — | Overridden to `true` by `bunext dev` automatically |
| `defaultCacheExpiry` | `number` | `3600` | Global page cache expiry in seconds | | `defaultCacheExpiry` | `number` | `3600` | Global page cache expiry in seconds |
| `middleware` | `(params: BunextConfigMiddlewareParams) => Response \| undefined \| Promise<...>` | — | Global middleware — see [Middleware](#middleware) | | `middleware` | `(params: BunextConfigMiddlewareParams) => Response \| undefined \| Promise<...>` | — | Global middleware — see [Middleware](#middleware) |
@@ -910,8 +914,34 @@ bun run server.ts
## Environment Variables ## Environment Variables
| Variable | Description | | Variable | Description |
| -------- | ------------------------------------------------------- | | ------------------ | ------------------------------------------------------------------------------------------------ |
| `PORT` | Override the server port (takes precedence over config) | | `PORT` | Override the server port (takes precedence over config) |
| `BUNEXT_PUBLIC_*` | Any env var prefixed with `BUNEXT_PUBLIC_` is exposed to the client via `window.process.env` |
### Public Environment Variables
Variables prefixed with `BUNEXT_PUBLIC_` are automatically injected into every page as `window.process.env`. You can also define public envs in config via `public_envs` (config values override env vars of the same name):
```bash
# .env
BUNEXT_PUBLIC_API_URL=https://api.example.com
```
```ts
// bunext.config.ts
const config: BunextConfig = {
public_envs: {
BUNEXT_PUBLIC_APP_NAME: "My App",
},
};
```
```tsx
// Client component — available after hydration
const apiUrl = window.process.env.BUNEXT_PUBLIC_API_URL;
```
`window.process.env` always includes `NODE_ENV` (`"development"` or `"production"`).
--- ---
+8
View File
@@ -12,6 +12,7 @@
"@types/react-dom": "^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",
@@ -26,6 +27,7 @@
}, },
"devDependencies": { "devDependencies": {
"@testing-library/dom": "^10.4.1", "@testing-library/dom": "^10.4.1",
"@types/chokidar": "^2.1.7",
"@types/lodash": "^4.17.24", "@types/lodash": "^4.17.24",
"@types/micromatch": "^4.0.10", "@types/micromatch": "^4.0.10",
"happy-dom": "^20.8.4", "happy-dom": "^20.8.4",
@@ -165,6 +167,8 @@
"@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="], "@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=="],
"@types/micromatch": ["@types/micromatch@4.0.10", "", { "dependencies": { "@types/braces": "*" } }, "sha512-5jOhFDElqr4DKTrTEbnW8DZ4Hz5LRUEmyrGpCMrD/NphYv3nUnaF08xmSLx1rGGnyEs/kFnhiw6dCgcDqMr5PQ=="], "@types/micromatch": ["@types/micromatch@4.0.10", "", { "dependencies": { "@types/braces": "*" } }, "sha512-5jOhFDElqr4DKTrTEbnW8DZ4Hz5LRUEmyrGpCMrD/NphYv3nUnaF08xmSLx1rGGnyEs/kFnhiw6dCgcDqMr5PQ=="],
@@ -195,6 +199,8 @@
"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=="],
@@ -291,6 +297,8 @@
"react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], "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=="],
+4 -10
View File
@@ -1,9 +1,8 @@
import { Command } from "commander"; import { Command } from "commander";
import { log } from "../../utils/log"; import { log } from "../../utils/log";
import init from "../../functions/init";
import grabDirNames from "../../utils/grab-dir-names"; import grabDirNames from "../../utils/grab-dir-names";
import { rmSync } from "fs"; import { rmSync } from "fs";
import allPagesESBuildContextBundler from "../../functions/bundler/all-pages-esbuild-context-bundler"; import bunextInit from "../../functions/bunext-init";
const { HYDRATION_DST_DIR, BUNX_CWD_PAGES_REWRITE_DIR } = grabDirNames(); const { HYDRATION_DST_DIR, BUNX_CWD_PAGES_REWRITE_DIR } = grabDirNames();
export default function () { export default function () {
return new Command("build") return new Command("build")
@@ -14,14 +13,9 @@ export default function () {
rmSync(BUNX_CWD_PAGES_REWRITE_DIR, { recursive: true }); rmSync(BUNX_CWD_PAGES_REWRITE_DIR, { recursive: true });
} }
catch (error) { } catch (error) { }
global.SKIPPED_BROWSER_MODULES = new Set(); global.BUNEXT_SKIPPED_BROWSER_MODULES = new Set();
// await rewritePagesModule(); await bunextInit({ build_only: true });
await init(); log.success("Modules Built Successfully!");
log.banner();
log.build("Building Project ...");
// await allPagesBunBundler();
// await allPagesBundler();
await allPagesESBuildContextBundler();
process.exit(); process.exit();
}); });
} }
+13 -2
View File
@@ -4,11 +4,22 @@ import bunextInit from "../../functions/bunext-init";
import grabDirNames from "../../utils/grab-dir-names"; import grabDirNames from "../../utils/grab-dir-names";
import { rmSync } from "fs"; import { rmSync } from "fs";
const { HYDRATION_DST_DIR, BUNX_CWD_PAGES_REWRITE_DIR } = grabDirNames(); 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 ..."); log.info("Running development server ...");
try { try {
rmSync(HYDRATION_DST_DIR, { recursive: true }); rmSync(HYDRATION_DST_DIR, { recursive: true });
rmSync(BUNX_CWD_PAGES_REWRITE_DIR, { recursive: true }); rmSync(BUNX_CWD_PAGES_REWRITE_DIR, { recursive: true });
} }
catch (error) { } catch (error) { }
await bunextInit(); try {
await startServer(); await bunextInit();
await startServer();
}
catch (error) {
log.error(`Failed to start development server: ${error}`);
}
+23 -4
View File
@@ -2,6 +2,7 @@ import { Command } from "commander";
import path from "path"; import path from "path";
import grabDirNames from "../../utils/grab-dir-names"; import grabDirNames from "../../utils/grab-dir-names";
import writeErrorFile from "../../functions/write-error-file"; import writeErrorFile from "../../functions/write-error-file";
import { existsSync } from "fs";
let retries = 0; let retries = 0;
let timeout; let timeout;
const MAX_RETRIES = 5; const MAX_RETRIES = 5;
@@ -19,8 +20,12 @@ async function dev() {
process.exit(1); process.exit(1);
} }
const dev_spawn_file = path.resolve(__dirname, "dev-spawn.ts"); 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 = { const spawn_options = {
cmd: ["bun", dev_spawn_file], cmd: ["bun", final_spawn_file],
stdio: ["inherit", "inherit", "inherit"], stdio: ["inherit", "inherit", "inherit"],
async onExit(subprocess, exitCode, signalCode, error) { async onExit(subprocess, exitCode, signalCode, error) {
writeErrorFile({ exitCode, error }); writeErrorFile({ exitCode, error });
@@ -30,13 +35,27 @@ async function dev() {
NODE_ENV: "development", NODE_ENV: "development",
}, },
}; };
let dev_process = Bun.spawn(spawn_options); let dev_process;
try {
dev_process = Bun.spawn(spawn_options);
}
catch (error) {
console.error(`Failed to start dev process:`, error);
retries++; retries++;
timeout = setTimeout(() => { timeout = setTimeout(() => {
retries = 0; retries = 0;
}, 10000); }, 10000);
const exited = await dev_process.exited;
if (exited) {
return await dev(); 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);
} }
+36 -5
View File
@@ -1,6 +1,10 @@
import { Command } from "commander"; import { Command } from "commander";
import path from "path"; import path from "path";
import writeErrorFile from "../../functions/write-error-file"; 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("start") return new Command("start")
.description("Start production server") .description("Start production server")
@@ -9,9 +13,18 @@ export default function () {
}); });
} }
async function start() { async function start() {
const dev_spawn_file = path.resolve(__dirname, "prod-spawn.ts"); 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 = { const spawn_options = {
cmd: ["bun", dev_spawn_file], cmd: ["bun", final_spawn_file],
stdio: ["inherit", "inherit", "inherit"], stdio: ["inherit", "inherit", "inherit"],
onExit(subprocess, exitCode, signalCode, error) { onExit(subprocess, exitCode, signalCode, error) {
writeErrorFile({ exitCode, error }); writeErrorFile({ exitCode, error });
@@ -21,9 +34,27 @@ async function start() {
NODE_ENV: "production", NODE_ENV: "production",
}, },
}; };
let dev_process = Bun.spawn(spawn_options); let dev_process;
const exited = await dev_process.exited; try {
if (exited) { 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(); 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);
} }
@@ -3,6 +3,8 @@ type Params = {
post_build_fn?: (params: { post_build_fn?: (params: {
artifacts: BundlerCTXMap[]; artifacts: BundlerCTXMap[];
}) => Promise<void> | void; }) => Promise<void> | void;
build_only?: boolean;
start?: boolean;
}; };
export default function allPagesESBuildContextBundler(params?: Params): Promise<void>; export default function allPagesESBuildContextBundler(params?: Params): Promise<void>;
export {}; export {};
@@ -13,7 +13,7 @@ export default async function allPagesESBuildContextBundler(params) {
try { try {
const did_process_exit_because_of_bundler_error = existsSync(BUNX_BUNDLER_ERROR_EXIT_FILE); const did_process_exit_because_of_bundler_error = existsSync(BUNX_BUNDLER_ERROR_EXIT_FILE);
const pages = grabAllPages({ exclude_api: true }); const pages = grabAllPages({ exclude_api: true });
global.PAGE_FILES = pages; global.BUNEXT_PAGE_FILES = pages;
const dev = isDevelopment(); const dev = isDevelopment();
const entryToPage = new Map(); const entryToPage = new Map();
for (const page of pages) { for (const page of pages) {
@@ -28,7 +28,7 @@ export default async function allPagesESBuildContextBundler(params) {
entryToPage.set(entryFile, { ...page, tsx }); entryToPage.set(entryFile, { ...page, tsx });
} }
const entryPoints = [...entryToPage.keys()].map((e) => `hydration-virtual:${e}`); const entryPoints = [...entryToPage.keys()].map((e) => `hydration-virtual:${e}`);
global.BUNDLER_CTX = await esbuild.context({ global.BUNEXT_BUNDLER_CTX = await esbuild.context({
entryPoints, entryPoints,
outdir: HYDRATION_DST_DIR, outdir: HYDRATION_DST_DIR,
bundle: true, bundle: true,
@@ -50,6 +50,7 @@ export default async function allPagesESBuildContextBundler(params) {
esbuildCTXArtifactTracker({ esbuildCTXArtifactTracker({
entryToPage, entryToPage,
post_build_fn: params?.post_build_fn, post_build_fn: params?.post_build_fn,
build_only: params?.build_only || params?.start,
}), }),
], ],
jsx: "automatic", jsx: "automatic",
@@ -61,13 +62,13 @@ export default async function allPagesESBuildContextBundler(params) {
"react-dom/client", "react-dom/client",
"react/jsx-runtime", "react/jsx-runtime",
"react/jsx-dev-runtime", "react/jsx-dev-runtime",
...(global.CONFIG.page_compiler_excludes || []), ...(global.BUNEXT_CONFIG.page_compiler_excludes || []),
], ],
logLevel: did_process_exit_because_of_bundler_error logLevel: did_process_exit_because_of_bundler_error
? "silent" ? "silent"
: undefined, : undefined,
}); });
await global.BUNDLER_CTX.rebuild(); await global.BUNEXT_BUNDLER_CTX.rebuild();
} }
catch (error) { catch (error) {
console.log(`ESBUILD Error =>`, error); console.log(`ESBUILD Error =>`, error);
+13 -8
View File
@@ -1,13 +1,18 @@
export default async function buildOnstartErrorHandler(params) { export default async function buildOnstartErrorHandler(params) {
// const error_msg = `Build Failed. Please check all your components and imports.`; // const error_msg = `Build Failed. Please check all your components and imports.`;
// log.error(error_msg); // log.error(error_msg);
global.BUNDLER_CTX_DISPOSED = true; if (global.BUNEXT_BUNDLER_CTX_DISPOSED) {
global.RECOMPILING = false; return;
global.IS_SERVER_COMPONENT = false; }
Promise.all([ // console.log(`Killing Bundler ...`);
global.SSR_BUNDLER_CTX?.dispose(), // console.log(`global.BUNEXT_BUNDLER_CTX_DISPOSED`, global.BUNEXT_BUNDLER_CTX_DISPOSED);
global.BUNDLER_CTX?.dispose(), 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.SSR_BUNDLER_CTX = undefined; global.BUNEXT_SSR_BUNDLER_CTX = undefined;
global.BUNDLER_CTX = undefined; global.BUNEXT_BUNDLER_CTX = undefined;
} }
+1 -1
View File
@@ -62,7 +62,7 @@ export default async function bunReactModulesBundler() {
}); });
rmSync(tmpDir, { force: true, recursive: true }); rmSync(tmpDir, { force: true, recursive: true });
const PUBLIC_ROOT = BUNEXT_VENDOR_DIR.replace(BUNX_CWD_DIR, "/.bunext"); const PUBLIC_ROOT = BUNEXT_VENDOR_DIR.replace(BUNX_CWD_DIR, "/.bunext");
global.REACT_IMPORTS_MAP = { global.BUNEXT_REACT_IMPORTS_MAP = {
imports: { imports: {
react: `${PUBLIC_ROOT}/react.js`, react: `${PUBLIC_ROOT}/react.js`,
"react-dom": `${PUBLIC_ROOT}/react-dom.js`, "react-dom": `${PUBLIC_ROOT}/react-dom.js`,
+8 -2
View File
@@ -16,7 +16,7 @@ export default async function pagesSSRBundler(params) {
include_server: true, include_server: true,
}); });
const dev = isDevelopment(); const dev = isDevelopment();
const config = global.CONFIG; const config = global.BUNEXT_CONFIG;
try { try {
writeFileSync(path.join(BUNX_TMP_DIR, "ssr-pages.json"), JSON.stringify(pages, null, 4)); writeFileSync(path.join(BUNX_TMP_DIR, "ssr-pages.json"), JSON.stringify(pages, null, 4));
} }
@@ -48,6 +48,7 @@ export default async function pagesSSRBundler(params) {
writeFileSync(path.join(BUNX_TMP_DIR, "ssr-entrypoints.json"), JSON.stringify(entryPoints, null, 4)); writeFileSync(path.join(BUNX_TMP_DIR, "ssr-entrypoints.json"), JSON.stringify(entryPoints, null, 4));
} }
catch (error) { } catch (error) { }
try {
await esbuild.build({ await esbuild.build({
entryPoints, entryPoints,
outdir: BUNX_CWD_MODULE_CACHE_DIR, outdir: BUNX_CWD_MODULE_CACHE_DIR,
@@ -78,11 +79,16 @@ export default async function pagesSSRBundler(params) {
"react/jsx-runtime", "react/jsx-runtime",
"react/jsx-dev-runtime", "react/jsx-dev-runtime",
"bun:*", "bun:*",
"bun",
"sqlite-vec", "sqlite-vec",
"better-sqlite3", "better-sqlite3",
...(config.ssr_compiler_excludes || []), ...(config.ssr_compiler_excludes || []),
], ],
splitting: true, splitting: true,
// logLevel: "silent",
}); });
}
catch (error) {
global.BUNEXT_SSR_BUNDLER_CTX_DISPOSED = true;
log.error(`SSR Bundler Error: ${error}`);
}
} }
+5 -5
View File
@@ -11,9 +11,9 @@ const { BUNX_CWD_MODULE_CACHE_DIR } = grabDirNames();
export default async function pagesSSRContextBundler(params) { export default async function pagesSSRContextBundler(params) {
const pages = grabAllPages(); const pages = grabAllPages();
const dev = isDevelopment(); const dev = isDevelopment();
if (global.SSR_BUNDLER_CTX) { if (global.BUNEXT_SSR_BUNDLER_CTX) {
await global.SSR_BUNDLER_CTX.dispose(); await global.BUNEXT_SSR_BUNDLER_CTX.dispose();
global.SSR_BUNDLER_CTX = undefined; global.BUNEXT_SSR_BUNDLER_CTX = undefined;
} }
const entryToPage = new Map(); const entryToPage = new Map();
const { root_file_path } = grabRootFilePath(); const { root_file_path } = grabRootFilePath();
@@ -32,7 +32,7 @@ export default async function pagesSSRContextBundler(params) {
entryToPage.set(page.local_path, { ...page, tsx }); entryToPage.set(page.local_path, { ...page, tsx });
} }
const entryPoints = [...entryToPage.keys()].map((e) => `ssr-virtual:${e}`); const entryPoints = [...entryToPage.keys()].map((e) => `ssr-virtual:${e}`);
global.SSR_BUNDLER_CTX = await esbuild.context({ global.BUNEXT_SSR_BUNDLER_CTX = await esbuild.context({
entryPoints, entryPoints,
outdir: BUNX_CWD_MODULE_CACHE_DIR, outdir: BUNX_CWD_MODULE_CACHE_DIR,
bundle: true, bundle: true,
@@ -65,5 +65,5 @@ export default async function pagesSSRContextBundler(params) {
], ],
// logLevel: "silent", // logLevel: "silent",
}); });
await global.SSR_BUNDLER_CTX.rebuild(); await global.BUNEXT_SSR_BUNDLER_CTX.rebuild();
} }
+3 -3
View File
@@ -5,7 +5,7 @@ const BunSkipNonBrowserPlugin = {
const skipFilter = /^(bun:|node:|fs$|path$|os$|crypto$|net$|events$|util$|tls$|url$|process$)/; const skipFilter = /^(bun:|node:|fs$|path$|os$|crypto$|net$|events$|util$|tls$|url$|process$)/;
// const skipped_modules = new Set<string>(); // const skipped_modules = new Set<string>();
build.onResolve({ filter: skipFilter }, (args) => { build.onResolve({ filter: skipFilter }, (args) => {
global.SKIPPED_BROWSER_MODULES.add(args.path); global.BUNEXT_SKIPPED_BROWSER_MODULES.add(args.path);
return { return {
path: args.path, path: args.path,
namespace: "skipped", namespace: "skipped",
@@ -13,8 +13,8 @@ const BunSkipNonBrowserPlugin = {
}; };
}); });
// build.onEnd(() => { // build.onEnd(() => {
// log.warn(`global.SKIPPED_BROWSER_MODULES`, [ // log.warn(`global.BUNEXT_SKIPPED_BROWSER_MODULES`, [
// ...global.SKIPPED_BROWSER_MODULES, // ...global.BUNEXT_SKIPPED_BROWSER_MODULES,
// ]); // ]);
// }); // });
// build.onResolve({ filter: /^[^./]/ }, (args) => { // build.onResolve({ filter: /^[^./]/ }, (args) => {
@@ -7,6 +7,7 @@ type Params = {
post_build_fn?: (params: { post_build_fn?: (params: {
artifacts: any[]; artifacts: any[];
}) => Promise<void> | void; }) => Promise<void> | void;
build_only?: boolean;
}; };
export default function esbuildCTXArtifactTracker({ entryToPage, post_build_fn, }: Params): Plugin; export default function esbuildCTXArtifactTracker({ entryToPage, post_build_fn, build_only, }: Params): Plugin;
export {}; export {};
@@ -11,37 +11,35 @@ import path from "path";
import cleanupLogsDirs from "../../cleanup-logs-dir"; import cleanupLogsDirs from "../../cleanup-logs-dir";
const { BUNX_BUNDLER_ERROR_EXIT_FILE, BUNX_ERROR_LOGS_DIR } = grabDirNames(); const { BUNX_BUNDLER_ERROR_EXIT_FILE, BUNX_ERROR_LOGS_DIR } = grabDirNames();
let build_start = 0; let build_start = 0;
let build_starts = 0;
const MAX_BUILD_STARTS = 2; const MAX_BUILD_STARTS = 2;
export default function esbuildCTXArtifactTracker({ entryToPage, post_build_fn, }) { export default function esbuildCTXArtifactTracker({ entryToPage, post_build_fn, build_only, }) {
const artifactTracker = { const artifactTracker = {
name: "artifact-tracker", name: "artifact-tracker",
setup(build) { setup(build) {
build.onStart(async () => { build.onStart(async () => {
build_starts++; global.BUNEXT_MAIN_CTX_BUILD_STARTS++;
build_start = performance.now(); build_start = performance.now();
const does_error_file_exist = existsSync(BUNX_BUNDLER_ERROR_EXIT_FILE); const does_error_file_exist = existsSync(BUNX_BUNDLER_ERROR_EXIT_FILE);
if (build_starts >= MAX_BUILD_STARTS && if (global.BUNEXT_MAIN_CTX_BUILD_STARTS >= MAX_BUILD_STARTS &&
!does_error_file_exist) { !does_error_file_exist) {
await buildOnstartErrorHandler(); await buildOnstartErrorHandler();
} }
}); });
build.onEnd((result) => { build.onEnd(async (result) => {
if (result.errors.length > 0) { if (result.errors.length > 0) {
global.RECOMPILING = false; global.BUNEXT_RECOMPILING = false;
global.IS_SERVER_COMPONENT = false; global.BUNEXT_IS_SERVER_COMPONENT = false;
build_starts = 0;
log.error(`Build errors:`); log.error(`Build errors:`);
for (const err of result.errors) { for (const err of result.errors) {
log.error(` ${err.text}${err.location ? ` (${err.location.file}:${err.location.line}:${err.location.column})` : ""}`); log.error(` ${err.text}${err.location ? ` (${err.location.file}:${err.location.line}:${err.location.column})` : ""}`);
} }
for (let i = global.HMR_CONTROLLERS.length - 1; i >= 0; i--) { for (let i = global.BUNEXT_HMR_CONTROLLERS.length - 1; i >= 0; i--) {
const controller = global.HMR_CONTROLLERS[i]; const controller = global.BUNEXT_HMR_CONTROLLERS[i];
try { try {
controller?.controller?.enqueue(`event: update\ndata: ${JSON.stringify({ reload: true })}\n\n`); controller?.controller?.enqueue(`event: update\ndata: ${JSON.stringify({ reload: true })}\n\n`);
} }
catch { catch {
global.HMR_CONTROLLERS.splice(i, 1); global.BUNEXT_HMR_CONTROLLERS.splice(i, 1);
} }
} }
return; return;
@@ -53,39 +51,52 @@ export default function esbuildCTXArtifactTracker({ entryToPage, post_build_fn,
if (artifacts?.[0] && artifacts.length > 0) { if (artifacts?.[0] && artifacts.length > 0) {
for (let i = 0; i < artifacts.length; i++) { for (let i = 0; i < artifacts.length; i++) {
const artifact = artifacts[i]; const artifact = artifacts[i];
if (artifact?.local_path && global.BUNDLER_CTX_MAP) { if (artifact?.local_path &&
global.BUNDLER_CTX_MAP[artifact.local_path] = global.BUNEXT_BUNDLER_CTX_MAP) {
_.merge(global.BUNDLER_CTX_MAP[artifact.local_path], artifact); global.BUNEXT_BUNDLER_CTX_MAP[artifact.local_path] =
_.merge(global.BUNEXT_BUNDLER_CTX_MAP[artifact.local_path], artifact);
} }
} }
post_build_fn?.({ artifacts });
} }
const elapsed = (performance.now() - build_start).toFixed(0); const elapsed = (performance.now() - build_start).toFixed(0);
log.success(`[Built] in ${elapsed}ms`); log.success(`[Built] in ${elapsed}ms`);
global.RECOMPILING = false; global.BUNEXT_MAIN_CTX_BUILD_STARTS = 0;
global.IS_SERVER_COMPONENT = false; global.BUNEXT_BUNDLER_CTX_DISPOSED = false;
build_starts = 0;
const does_error_file_exist = existsSync(BUNX_BUNDLER_ERROR_EXIT_FILE); const does_error_file_exist = existsSync(BUNX_BUNDLER_ERROR_EXIT_FILE);
if (does_error_file_exist) { // SSR must finish before HMR so server props are fresh
mkdirSync(BUNX_ERROR_LOGS_DIR, { recursive: true }); if (build_only) {
cpSync(BUNX_BUNDLER_ERROR_EXIT_FILE, path.join(BUNX_ERROR_LOGS_DIR, `${Date.now()}.log`));
rmSync(BUNX_BUNDLER_ERROR_EXIT_FILE, { force: true });
cleanupLogsDirs();
fullRebuild();
}
else {
try { try {
pagesSSRBundler(); await pagesSSRBundler();
} }
catch (error) { catch (error) {
log.error(`SSR Bundler Error: ${error}`); log.error(`SSR Bundler Error: ${error}`);
} }
} }
// if (global.SSR_BUNDLER_CTX) { else if (does_error_file_exist) {
// global.SSR_BUNDLER_CTX.rebuild(); mkdirSync(BUNX_ERROR_LOGS_DIR, { recursive: true });
// } else { cpSync(BUNX_BUNDLER_ERROR_EXIT_FILE, path.join(BUNX_ERROR_LOGS_DIR, `${Date.now()}.log`));
// pagesSSRContextBundler(); 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;
}); });
}, },
}; };
+17 -11
View File
@@ -15,16 +15,23 @@ export default function ssrCTXArtifactTracker({ entryToPage, post_build_fn, }) {
build_starts++; build_starts++;
build_start = performance.now(); build_start = performance.now();
if (build_starts == MAX_BUILD_STARTS) { if (build_starts == MAX_BUILD_STARTS) {
global.SSR_BUNDLER_CTX_DISPOSED = true; global.BUNEXT_SSR_BUNDLER_CTX_DISPOSED = true;
await global.SSR_BUNDLER_CTX?.dispose(); await global.BUNEXT_SSR_BUNDLER_CTX?.dispose();
global.SSR_BUNDLER_CTX = undefined; global.BUNEXT_SSR_BUNDLER_CTX = undefined;
} }
}); });
build.onEnd((result) => { build.onEnd(async (result) => {
if (result.errors.length > 0) { if (result.errors.length > 0) {
global.SSR_BUNDLER_CTX_DISPOSED = false; 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; build_starts = 0;
console.log("SSR Build errors:", result.errors); 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; return;
} }
const artifacts = grabArtifactsFromBundledResults({ const artifacts = grabArtifactsFromBundledResults({
@@ -36,9 +43,8 @@ export default function ssrCTXArtifactTracker({ entryToPage, post_build_fn, }) {
for (let i = 0; i < artifacts.length; i++) { for (let i = 0; i < artifacts.length; i++) {
const artifact = artifacts[i]; const artifact = artifacts[i];
if (artifact?.local_path && if (artifact?.local_path &&
global.SSR_BUNDLER_CTX_MAP) { global.BUNEXT_SSR_BUNDLER_CTX_MAP) {
global.SSR_BUNDLER_CTX_MAP[artifact.local_path] = global.BUNEXT_SSR_BUNDLER_CTX_MAP[artifact.local_path] = artifact;
artifact;
} }
} }
// post_build_fn?.({ artifacts }); // post_build_fn?.({ artifacts });
@@ -48,10 +54,10 @@ export default function ssrCTXArtifactTracker({ entryToPage, post_build_fn, }) {
// log.success(`SSR [Built] in ${elapsed}ms`); // log.success(`SSR [Built] in ${elapsed}ms`);
} }
try { try {
writeFileSync(path.join(BUNX_TMP_DIR, "ctx-map.json"), JSON.stringify(global.SSR_BUNDLER_CTX_MAP, null, 4)); writeFileSync(path.join(BUNX_TMP_DIR, "ctx-map.json"), JSON.stringify(global.BUNEXT_SSR_BUNDLER_CTX_MAP, null, 4));
} }
catch (error) { } catch (error) { }
global.SSR_BUNDLER_CTX_DISPOSED = false; global.BUNEXT_SSR_BUNDLER_CTX_DISPOSED = false;
}); });
}, },
}; };
@@ -1,5 +1,4 @@
import path from "path"; import path from "path";
import { log } from "../../../utils/log";
export default function virtualFilesPlugin({ entryToPage }) { export default function virtualFilesPlugin({ entryToPage }) {
const virtualPlugin = { const virtualPlugin = {
name: "virtual-hydration", name: "virtual-hydration",
+1 -1
View File
@@ -65,7 +65,7 @@ export default async function reactModulesBundler() {
}); });
rmSync(tmpDir, { force: true, recursive: true }); rmSync(tmpDir, { force: true, recursive: true });
const PUBLIC_ROOT = BUNEXT_VENDOR_DIR.replace(BUNX_CWD_DIR, "/.bunext"); const PUBLIC_ROOT = BUNEXT_VENDOR_DIR.replace(BUNX_CWD_DIR, "/.bunext");
global.REACT_IMPORTS_MAP = { global.BUNEXT_REACT_IMPORTS_MAP = {
imports: { imports: {
react: `${PUBLIC_ROOT}/react.js`, react: `${PUBLIC_ROOT}/react.js`,
"react-dom": `${PUBLIC_ROOT}/react-dom.js`, "react-dom": `${PUBLIC_ROOT}/react-dom.js`,
+2 -2
View File
@@ -8,8 +8,8 @@ export default async function recordArtifacts({ artifacts, page_file_paths, }) {
artifacts_map[artifact.local_path] = artifact; artifacts_map[artifact.local_path] = artifact;
} }
} }
if (global.BUNDLER_CTX_MAP) { if (global.BUNEXT_BUNDLER_CTX_MAP) {
global.BUNDLER_CTX_MAP = _.merge(global.BUNDLER_CTX_MAP, artifacts_map); global.BUNEXT_BUNDLER_CTX_MAP = _.merge(global.BUNEXT_BUNDLER_CTX_MAP, artifacts_map);
} }
// await Bun.write( // await Bun.write(
// HYDRATION_DST_DIR_MAP_JSON_FILE, // HYDRATION_DST_DIR_MAP_JSON_FILE,
+36 -31
View File
@@ -1,49 +1,54 @@
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 grabDirNames from "../utils/grab-dir-names"; import { type DirNames } from "../utils/grab-dir-names";
import { type FSWatcher } from "fs";
import type { BuildContext } from "esbuild"; import type { BuildContext } from "esbuild";
import grabConstants from "../utils/grab-constants"; import grabConstants from "../utils/grab-constants";
import type { FSWatcher } from "fs";
/** /**
* # Declare Global Variables * # Declare Global Variables
*/ */
declare global { declare global {
var CONFIG: BunextConfig; var BUNEXT_CONFIG: BunextConfig;
var SERVER: Server<any> | undefined; var BUNEXT_SERVER: Server<any> | undefined;
var RECOMPILING: boolean; var BUNEXT_RECOMPILING: boolean;
var BUILDING_SSR: boolean; var BUNEXT_BUILDING_SSR: boolean;
var IS_SERVER_COMPONENT: boolean; var BUNEXT_IS_SERVER_COMPONENT: boolean;
var WATCHER_TIMEOUT: any; var BUNEXT_WATCHER_TIMEOUT: any;
var ROUTER: FileSystemRouter; var BUNEXT_ROUTER: FileSystemRouter;
var HMR_CONTROLLERS: GlobalHMRControllerObject[]; var BUNEXT_HMR_CONTROLLERS: GlobalHMRControllerObject[];
var LAST_BUILD_TIME: number; var BUNEXT_LAST_BUILD_TIME: number;
var BUNDLER_CTX_MAP: { var BUNEXT_BUNDLER_CTX_MAP: {
[k: string]: BundlerCTXMap; [k: string]: BundlerCTXMap;
}; };
var SSR_BUNDLER_CTX_MAP: { var BUNEXT_SSR_BUNDLER_CTX_MAP: {
[k: string]: BundlerCTXMap; [k: string]: BundlerCTXMap;
}; };
var BUNDLER_REBUILDS: 0; var BUNEXT_BUNDLER_REBUILDS: 0;
var PAGES_SRC_WATCHER: FSWatcher | undefined; var BUNEXT_PAGES_SRC_WATCHER: FSWatcher | undefined;
var CURRENT_VERSION: string | undefined; var BUNEXT_CURRENT_VERSION: string | undefined;
var PAGE_FILES: PageFiles[]; var BUNEXT_PAGE_FILES: PageFiles[];
var ROOT_FILE_UPDATED: boolean; var BUNEXT_ROOT_FILE_UPDATED: boolean;
var SKIPPED_BROWSER_MODULES: Set<string>; var BUNEXT_SKIPPED_BROWSER_MODULES: Set<string>;
var BUNDLER_CTX: BuildContext | undefined; var BUNEXT_BUNDLER_CTX: BuildContext | undefined;
var SSR_BUNDLER_CTX: BuildContext | undefined; var BUNEXT_SSR_BUNDLER_CTX: BuildContext | undefined;
var DIR_NAMES: ReturnType<typeof grabDirNames>; var BUNEXT_DIR_NAMES: DirNames;
var REACT_IMPORTS_MAP: { var BUNEXT_REACT_IMPORTS_MAP: {
imports: Record<string, string>; imports: Record<string, string>;
}; };
var REACT_DOM_SERVER: any; var BUNEXT_REACT_DOM_SERVER: any;
var REACT_DOM_MODULE_CACHE: Map<string, { var BUNEXT_REACT_DOM_MODULE_CACHE: Map<string, {
main: any; main: any;
css: string; css: string;
}>; }>;
var BUNDLER_CTX_DISPOSED: boolean | undefined; var BUNEXT_BUNDLER_CTX_DISPOSED: boolean | undefined;
var SSR_BUNDLER_CTX_DISPOSED: boolean | undefined; var BUNEXT_SSR_BUNDLER_CTX_DISPOSED: boolean | undefined;
var REBUILD_RETRIES: number; var BUNEXT_REBUILD_RETRIES: number;
var IS_404_PAGE: boolean; var BUNEXT_IS_404_PAGE: boolean;
var CONSTANTS: ReturnType<typeof grabConstants>; 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 {};
+26 -22
View File
@@ -1,50 +1,54 @@
import grabDirNames from "../utils/grab-dir-names"; import grabDirNames, {} from "../utils/grab-dir-names";
import {} from "fs";
import init from "./init"; import init from "./init";
import isDevelopment from "../utils/is-development"; import isDevelopment from "../utils/is-development";
import { log } from "../utils/log"; import { log } from "../utils/log";
import cron from "./server/cron"; import cron from "./server/cron";
import watcherEsbuildCTX from "./server/watcher-esbuild-ctx";
import allPagesESBuildContextBundler from "./bundler/all-pages-esbuild-context-bundler"; import allPagesESBuildContextBundler from "./bundler/all-pages-esbuild-context-bundler";
import serverPostBuildFn from "./server/server-post-build-fn"; import serverPostBuildFn from "./server/server-post-build-fn";
import reactModulesBundler from "./bundler/react-modules-bundler"; import reactModulesBundler from "./bundler/react-modules-bundler";
import grabConstants from "../utils/grab-constants"; import grabConstants from "../utils/grab-constants";
import watcherEsbuildCTX from "./server/watcher-esbuild-ctx";
const dirNames = grabDirNames(); const dirNames = grabDirNames();
const { PAGES_DIR } = dirNames; const { PAGES_DIR } = dirNames;
export default async function bunextInit() { export default async function bunextInit(params) {
global.HMR_CONTROLLERS = []; global.BUNEXT_HMR_CONTROLLERS = [];
global.BUNDLER_CTX_MAP = {}; global.BUNEXT_BUNDLER_CTX_MAP = {};
global.SSR_BUNDLER_CTX_MAP = {}; global.BUNEXT_SSR_BUNDLER_CTX_MAP = {};
// global.API_ROUTES_BUNDLER_CTX_MAP = {}; // global.BUNEXT_API_ROUTES_BUNDLER_CTX_MAP = {};
global.BUNDLER_REBUILDS = 0; global.BUNEXT_BUNDLER_REBUILDS = 0;
global.REBUILD_RETRIES = 0; global.BUNEXT_REBUILD_RETRIES = 0;
global.PAGE_FILES = []; global.BUNEXT_PAGE_FILES = [];
global.SKIPPED_BROWSER_MODULES = new Set(); global.BUNEXT_SKIPPED_BROWSER_MODULES = new Set();
global.DIR_NAMES = dirNames; global.BUNEXT_DIR_NAMES = dirNames;
global.REACT_IMPORTS_MAP = { imports: {} }; global.BUNEXT_REACT_IMPORTS_MAP = { imports: {} };
global.REACT_DOM_MODULE_CACHE = new Map(); global.BUNEXT_REACT_DOM_MODULE_CACHE = new Map();
log.banner(); global.BUNEXT_MAIN_CTX_BUILD_STARTS = 0;
await init(); await init();
global.CONSTANTS = grabConstants(); log.banner();
global.BUNEXT_CONSTANTS = grabConstants();
await reactModulesBundler(); 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) {
log.build(`Building Modules ...`);
await allPagesESBuildContextBundler();
}
else if (is_dev) {
log.build(`Building Modules ...`); log.build(`Building Modules ...`);
await allPagesESBuildContextBundler({ await allPagesESBuildContextBundler({
post_build_fn: () => { post_build_fn: async () => {
serverPostBuildFn(); await serverPostBuildFn();
}, },
}); });
watcherEsbuildCTX(); watcherEsbuildCTX();
} }
else { else {
log.build(`Building Modules ...`); log.build(`Building Modules ...`);
await allPagesESBuildContextBundler(); await allPagesESBuildContextBundler({ start: true });
cron(); cron();
} }
} }
+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) {
+1 -1
View File
@@ -9,7 +9,7 @@ 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.default_cache_expiry || 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;
+2 -2
View File
@@ -24,7 +24,7 @@ export default async function () {
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);
@@ -45,7 +45,7 @@ export default async function () {
} }
} }
const config = (await grabConfig()) || {}; const config = (await grabConfig()) || {};
global.CONFIG = { global.BUNEXT_CONFIG = {
...config, ...config,
development: is_dev, development: is_dev,
}; };
+13 -2
View File
@@ -8,6 +8,8 @@ import handleBunextPublicAssets from "./handle-bunext-public-assets";
import checkExcludedPatterns from "../../utils/check-excluded-patterns"; import checkExcludedPatterns from "../../utils/check-excluded-patterns";
import { AppData } from "../../data/app-data"; import { AppData } from "../../data/app-data";
import fullRebuild from "./full-rebuild"; import fullRebuild from "./full-rebuild";
const HMR_RETRY_COOLDOWN_MS = 5000;
let lastHmrRetryTime = 0;
export default async function bunextRequestHandler({ req: initial_req, server, }) { 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();
@@ -17,8 +19,8 @@ export default async function bunextRequestHandler({ req: initial_req, server, }
return Response.json({ success: false, msg: `Invalid Path` }); return Response.json({ success: false, msg: `Invalid Path` });
} }
let response = undefined; let response = undefined;
if (global.CONSTANTS.config?.middleware) { if (global.BUNEXT_CONSTANTS.config?.middleware) {
const middleware_res = await global.CONSTANTS.config.middleware({ const middleware_res = await global.BUNEXT_CONSTANTS.config.middleware({
req: initial_req, req: initial_req,
url, url,
}); });
@@ -30,6 +32,11 @@ export default async function bunextRequestHandler({ req: initial_req, server, }
} }
} }
if (is_dev && url.pathname == AppData["BunextHMRRetryRoute"]) { if (is_dev && url.pathname == AppData["BunextHMRRetryRoute"]) {
const now = Date.now();
if (now - lastHmrRetryTime < HMR_RETRY_COOLDOWN_MS) {
return new Response("Too Many Requests", { status: 429 });
}
lastHmrRetryTime = now;
await fullRebuild({ msg: `HMR Retry Rebuild ...` }); await fullRebuild({ msg: `HMR Retry Rebuild ...` });
return new Response("Modules Rebuilt"); return new Response("Modules Rebuilt");
} }
@@ -60,8 +67,12 @@ export default async function bunextRequestHandler({ req: initial_req, server, }
return response; return response;
} }
catch (error) { catch (error) {
if (is_dev) {
return new Response(`Server Error: ${error.message}`, { return new Response(`Server Error: ${error.message}`, {
status: 500, 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();
}
}
+18 -15
View File
@@ -1,34 +1,37 @@
import { log } from "../../utils/log"; import { log } from "../../utils/log";
import allPagesESBuildContextBundler from "../bundler/all-pages-esbuild-context-bundler"; import allPagesESBuildContextBundler from "../bundler/all-pages-esbuild-context-bundler";
import pagesSSRBundler from "../bundler/pages-ssr-bundler";
import serverPostBuildFn from "./server-post-build-fn"; import serverPostBuildFn from "./server-post-build-fn";
import watcherEsbuildCTX from "./watcher-esbuild-ctx"; import watcherEsbuildCTX from "./watcher-esbuild-ctx";
export default async function fullRebuild(params) { export default async function fullRebuild(params) {
try { try {
const { msg } = params || {}; const { msg } = params || {};
global.RECOMPILING = true; global.BUNEXT_RECOMPILING = true;
if (msg) { if (msg) {
log.watch(msg); log.watch(msg);
} }
global.ROUTER.reload(); global.BUNEXT_ROUTER.reload();
await global.BUNDLER_CTX?.dispose(); try {
global.BUNDLER_CTX = undefined; await global.BUNEXT_BUNDLER_CTX?.dispose();
await global.SSR_BUNDLER_CTX?.dispose(); global.BUNEXT_BUNDLER_CTX = undefined;
global.SSR_BUNDLER_CTX = undefined; await global.BUNEXT_SSR_BUNDLER_CTX?.dispose();
await pagesSSRBundler(); global.BUNEXT_SSR_BUNDLER_CTX = undefined;
allPagesESBuildContextBundler({ }
post_build_fn: () => { catch (error) { }
serverPostBuildFn(); await allPagesESBuildContextBundler({
post_build_fn: async () => {
await serverPostBuildFn();
}, },
}); });
} }
catch (error) { catch (error) {
global.RECOMPILING = false;
global.IS_SERVER_COMPONENT = false;
log.error(error); log.error(error);
} }
if (global.PAGES_SRC_WATCHER) { finally {
global.PAGES_SRC_WATCHER.close(); global.BUNEXT_RECOMPILING = false;
global.BUNEXT_IS_SERVER_COMPONENT = false;
}
if (global.BUNEXT_PAGES_SRC_WATCHER) {
global.BUNEXT_PAGES_SRC_WATCHER.close();
watcherEsbuildCTX(); watcherEsbuildCTX();
} }
} }
+2 -1
View File
@@ -2,13 +2,14 @@ import grabDirNames from "../../utils/grab-dir-names";
import path from "path"; import path from "path";
import isDevelopment from "../../utils/is-development"; import isDevelopment from "../../utils/is-development";
import { readFileResponse } from "./handle-public"; import { readFileResponse } from "./handle-public";
import isSafePath from "../../utils/is-safe-path";
const { BUNEXT_PUBLIC_DIR } = grabDirNames(); const { BUNEXT_PUBLIC_DIR } = grabDirNames();
export default async function ({ req }) { export default async function ({ req }) {
try { try {
const is_dev = isDevelopment(); const is_dev = isDevelopment();
const url = new URL(req.url); const url = new URL(req.url);
const file_path = path.join(BUNEXT_PUBLIC_DIR, url.pathname.replace(/\/\.bunext\/public\//, "")); const file_path = path.join(BUNEXT_PUBLIC_DIR, url.pathname.replace(/\/\.bunext\/public\//, ""));
if (!file_path.startsWith(BUNEXT_PUBLIC_DIR + path.sep)) { if (!isSafePath({ filePath: file_path, allowedDir: BUNEXT_PUBLIC_DIR })) {
return new Response("Forbidden", { status: 403 }); return new Response("Forbidden", { status: 403 });
} }
return readFileResponse({ return readFileResponse({
+7 -2
View File
@@ -2,13 +2,14 @@ import grabDirNames from "../../utils/grab-dir-names";
import path from "path"; import path from "path";
import isDevelopment from "../../utils/is-development"; import isDevelopment from "../../utils/is-development";
import { existsSync } from "fs"; import { existsSync } from "fs";
import isSafePath from "../../utils/is-safe-path";
const { PUBLIC_DIR } = grabDirNames(); const { PUBLIC_DIR } = grabDirNames();
export default async function ({ req }) { export default async function ({ req }) {
try { try {
const is_dev = isDevelopment(); const is_dev = isDevelopment();
const url = new URL(req.url); const url = new URL(req.url);
const file_path = path.join(PUBLIC_DIR, url.pathname); const file_path = path.join(PUBLIC_DIR, url.pathname);
if (!file_path.startsWith(PUBLIC_DIR + path.sep)) { if (!isSafePath({ filePath: file_path, allowedDir: PUBLIC_DIR })) {
return new Response("Forbidden", { status: 403 }); return new Response("Forbidden", { status: 403 });
} }
if (!existsSync(file_path)) { if (!existsSync(file_path)) {
@@ -17,7 +18,11 @@ export default async function ({ req }) {
}); });
} }
const file = Bun.file(file_path); const file = Bun.file(file_path);
return new Response(file); const headers = new Headers();
if (!is_dev) {
headers.set("Cache-Control", "public, max-age=3600");
}
return new Response(file, { headers });
} }
catch (error) { catch (error) {
return new Response(`File Not Found`, { return new Response(`File Not Found`, {
+24 -9
View File
@@ -1,18 +1,36 @@
function removeController(controller) {
const idx = global.BUNEXT_HMR_CONTROLLERS.findIndex((c) => c.controller == controller);
if (typeof idx == "number" && idx >= 0) {
global.BUNEXT_HMR_CONTROLLERS.splice(idx, 1);
}
}
export default async function ({ req }) { export default async function ({ req }) {
const referer_url = new URL(req.headers.get("referer") || ""); const referer = req.headers.get("referer");
const match = global.ROUTER.match(referer_url.pathname); const page_cookie = req.headers.get("cookie");
if (!referer) {
return new Response("Missing Referer Header", { status: 400 });
}
let referer_url;
try {
referer_url = new URL(referer);
}
catch {
return new Response("Invalid Referer Header", { status: 400 });
}
const match = global.BUNEXT_ROUTER.match(referer_url.pathname);
const target_map = match?.filePath const target_map = match?.filePath
? global.BUNDLER_CTX_MAP?.[match.filePath] ? global.BUNEXT_BUNDLER_CTX_MAP?.[match.filePath]
: undefined; : undefined;
let controller; let controller;
let heartbeat; let heartbeat;
const stream = new ReadableStream({ const stream = new ReadableStream({
start(c) { start(c) {
controller = c; controller = c;
global.HMR_CONTROLLERS.push({ global.BUNEXT_HMR_CONTROLLERS.push({
controller: c, controller: c,
page_url: referer_url.href, page_url: referer_url.href,
target_map, target_map,
page_cookie,
}); });
heartbeat = setInterval(() => { heartbeat = setInterval(() => {
try { try {
@@ -20,16 +38,13 @@ export default async function ({ req }) {
} }
catch { catch {
clearInterval(heartbeat); clearInterval(heartbeat);
removeController(controller);
} }
}, 5000); }, 5000);
}, },
cancel() { cancel() {
clearInterval(heartbeat); clearInterval(heartbeat);
const targetControllerIndex = global.HMR_CONTROLLERS.findIndex((c) => c.controller == controller); removeController(controller);
if (typeof targetControllerIndex == "number" &&
targetControllerIndex >= 0) {
global.HMR_CONTROLLERS.splice(targetControllerIndex, 1);
}
}, },
}); });
return new Response(stream, { return new Response(stream, {
+5 -1
View File
@@ -2,13 +2,14 @@ import grabDirNames from "../../utils/grab-dir-names";
import path from "path"; import path from "path";
import isDevelopment from "../../utils/is-development"; import isDevelopment from "../../utils/is-development";
import { existsSync } from "fs"; import { existsSync } from "fs";
import isSafePath from "../../utils/is-safe-path";
const { PUBLIC_DIR } = grabDirNames(); const { PUBLIC_DIR } = grabDirNames();
export default async function ({ req }) { export default async function ({ req }) {
try { try {
const is_dev = isDevelopment(); const is_dev = isDevelopment();
const url = new URL(req.url); const url = new URL(req.url);
const file_path = path.join(PUBLIC_DIR, url.pathname.replace(/^\/public/, "")); const file_path = path.join(PUBLIC_DIR, url.pathname.replace(/^\/public/, ""));
if (!file_path.startsWith(PUBLIC_DIR + path.sep)) { if (!isSafePath({ filePath: file_path, allowedDir: PUBLIC_DIR })) {
return new Response("Forbidden", { status: 403 }); return new Response("Forbidden", { status: 403 });
} }
return readFileResponse({ file_path }); return readFileResponse({ file_path });
@@ -33,6 +34,9 @@ export function readFileResponse({ file_path, cache }) {
else if (cache?.duration) { else if (cache?.duration) {
headers.set("Cache-Control", `public, max-age=${cache.duration}`); headers.set("Cache-Control", `public, max-age=${cache.duration}`);
} }
else if (!isDevelopment()) {
headers.set("Cache-Control", "public, max-age=3600");
}
return new Response(file, { return new Response(file, {
headers, headers,
}); });
+21 -5
View File
@@ -30,8 +30,8 @@ export default async function ({ req }) {
}); });
let module; let module;
const now = Date.now(); const now = Date.now();
if (is_dev && global.SSR_BUNDLER_CTX_MAP?.[match.filePath]?.path) { if (is_dev && global.BUNEXT_SSR_BUNDLER_CTX_MAP?.[match.filePath]?.path) {
const target_import = path.join(ROOT_DIR, global.SSR_BUNDLER_CTX_MAP[match.filePath].path); const target_import = path.join(ROOT_DIR, global.BUNEXT_SSR_BUNDLER_CTX_MAP[match.filePath].path);
module = await import(`${target_import}?t=${now}`); module = await import(`${target_import}?t=${now}`);
} }
else { else {
@@ -41,12 +41,13 @@ export default async function ({ req }) {
module = await import(import_path); module = await import(import_path);
} }
const config = module.config; const config = module.config;
const maxBodyBytes = config?.max_request_body_mb
? config.max_request_body_mb * MBInBytes
: ServerDefaultRequestBodyLimitBytes;
const contentLength = req.headers.get("content-length"); const contentLength = req.headers.get("content-length");
if (contentLength) { if (contentLength) {
const size = parseInt(contentLength, 10); const size = parseInt(contentLength, 10);
if ((config?.max_request_body_mb && if (size > maxBodyBytes) {
size > config.max_request_body_mb * MBInBytes) ||
size > ServerDefaultRequestBodyLimitBytes) {
return Response.json({ return Response.json({
success: false, success: false,
msg: "Request Body Too Large!", msg: "Request Body Too Large!",
@@ -58,6 +59,21 @@ export default async function ({ req }) {
}); });
} }
} }
else if (req.method !== "GET" && req.method !== "HEAD") {
const body = await req.arrayBuffer();
if (body.byteLength > maxBodyBytes) {
return Response.json({
success: false,
msg: "Request Body Too Large!",
}, {
status: 413,
headers: {
"Content-Type": "application/json",
},
});
}
routeParams.body = JSON.parse(new TextDecoder().decode(body) || "{}");
}
const target_module = (module["default"] || const target_module = (module["default"] ||
module["handler"]); module["handler"]);
const res = await target_module?.({ const res = await target_module?.({
+28 -25
View File
@@ -1,49 +1,53 @@
import _ from "lodash"; import _ from "lodash";
import grabPageComponent from "./web-pages/grab-page-component"; import grabPageComponent from "./web-pages/grab-page-component";
export default async function serverPostBuildFn(params) { export default async function serverPostBuildFn(params) {
if (!global.HMR_CONTROLLERS?.[0] || !global.BUNDLER_CTX_MAP) { if (!global.BUNEXT_HMR_CONTROLLERS?.[0] || !global.BUNEXT_BUNDLER_CTX_MAP) {
return; return;
} }
const reload_payload = { reload: true }; const reload_payload = { reload: true };
const reload_enqueue = `event: update\ndata: ${JSON.stringify(reload_payload)}\n\n`; const reload_enqueue = `event: update\ndata: ${JSON.stringify(reload_payload)}\n\n`;
for (let i = global.HMR_CONTROLLERS.length - 1; i >= 0; i--) { for (let i = global.BUNEXT_HMR_CONTROLLERS.length - 1; i >= 0; i--) {
const controller = global.HMR_CONTROLLERS[i]; const controller = global.BUNEXT_HMR_CONTROLLERS[i];
if (!controller) { if (!controller) {
continue; continue;
} }
if (!controller.target_map?.local_path) { if (!controller.target_map?.local_path) {
// if (global.IS_404_PAGE) {
// controller.controller.enqueue(reload_enqueue);
// }
// if (!global.HMR_CONTROLLERS[i].page_reloaded) {
// controller.controller.enqueue(reload_enqueue);
// global.HMR_CONTROLLERS[i].page_reloaded = true;
// }
continue; continue;
} }
if (params?.reload_all_controllers) { if (params?.reload_all_controllers) {
try {
controller.controller.enqueue(reload_enqueue); controller.controller.enqueue(reload_enqueue);
}
catch {
global.BUNEXT_HMR_CONTROLLERS.splice(i, 1);
}
continue; continue;
} }
const target_artifact = global.BUNDLER_CTX_MAP[controller.target_map.local_path]; const target_artifact = global.BUNEXT_BUNDLER_CTX_MAP[controller.target_map.local_path];
if (!target_artifact.local_path) { if (!target_artifact?.local_path) {
try {
controller.controller.enqueue(reload_enqueue); controller.controller.enqueue(reload_enqueue);
}
catch {
global.BUNEXT_HMR_CONTROLLERS.splice(i, 1);
}
continue; continue;
} }
const mock_req = target_artifact.req const mock_req = target_artifact.req_url
? target_artifact.req.clone() ? new Request(target_artifact.req_url, {})
: new Request(controller.page_url); : new Request(controller.page_url);
const page_component = global.IS_SERVER_COMPONENT if (controller.page_cookie) {
? await grabPageComponent({ mock_req.headers.set("cookie", controller.page_cookie);
}
const page_component = await grabPageComponent({
req: mock_req, req: mock_req,
return_server_res_only: true, return_server_res_only: true,
is_hydration: true, is_hydration: true,
}) });
: {};
if (page_component instanceof Response) { if (page_component instanceof Response) {
continue; continue;
} }
const { serverRes } = page_component; const { serverRes } = page_component || {};
const final_artifact = { const final_artifact = {
..._.omit(controller, ["controller"]), ..._.omit(controller, ["controller"]),
target_map: target_artifact, target_map: target_artifact,
@@ -51,22 +55,21 @@ export default async function serverPostBuildFn(params) {
if (!target_artifact) { if (!target_artifact) {
delete final_artifact.target_map; delete final_artifact.target_map;
} }
if (serverRes) { // Always replace so prior error props cannot linger
final_artifact.page_props = serverRes; final_artifact.page_props = serverRes || {};
}
try { try {
let final_data = {}; let final_data = {};
if (global.ROOT_FILE_UPDATED) { if (global.BUNEXT_ROOT_FILE_UPDATED) {
final_data = reload_payload; final_data = reload_payload;
} }
else { else {
final_data = final_artifact; final_data = final_artifact;
} }
controller.controller.enqueue(`event: update\ndata: ${JSON.stringify(final_data)}\n\n`); controller.controller.enqueue(`event: update\ndata: ${JSON.stringify(final_data)}\n\n`);
global.ROOT_FILE_UPDATED = false; global.BUNEXT_ROOT_FILE_UPDATED = false;
} }
catch { catch {
global.HMR_CONTROLLERS.splice(i, 1); global.BUNEXT_HMR_CONTROLLERS.splice(i, 1);
} }
} }
} }
+12 -1
View File
@@ -1,10 +1,21 @@
import _ from "lodash"; import _ from "lodash";
import { log } from "../../utils/log"; import { log } from "../../utils/log";
import serverParamsGen from "./server-params-gen"; import serverParamsGen from "./server-params-gen";
import isDevelopment from "../../utils/is-development";
import watcherEsbuildCTX from "./watcher-esbuild-ctx";
export default async function startServer() { export default async function startServer() {
const serverParams = await serverParamsGen(); const serverParams = await serverParamsGen();
const server = Bun.serve(serverParams); const server = Bun.serve(serverParams);
global.SERVER = server; const is_dev = isDevelopment();
global.BUNEXT_SERVER = server;
log.server(`http://${server.hostname}:${server.port}`); log.server(`http://${server.hostname}:${server.port}`);
if (is_dev) {
setInterval(() => {
if (global.BUNEXT_PAGES_SRC_WATCHER) {
global.BUNEXT_PAGES_SRC_WATCHER.close();
watcherEsbuildCTX();
}
}, 5000);
}
return server; return server;
} }
+58 -24
View File
@@ -1,18 +1,39 @@
import { watch, existsSync, statSync } from "fs"; import { watch, existsSync, statSync, glob } from "fs";
import path from "path"; import path from "path";
import grabDirNames from "../../utils/grab-dir-names"; import grabDirNames from "../../utils/grab-dir-names";
import fullRebuild from "./full-rebuild"; import fullRebuild from "./full-rebuild";
import { AppData } from "../../data/app-data"; import { AppData } from "../../data/app-data";
import checkExcludedPatterns from "../../utils/check-excluded-patterns"; import checkExcludedPatterns from "../../utils/check-excluded-patterns";
import pagesSSRBundler from "../bundler/pages-ssr-bundler"; import pagesSSRBundler from "../bundler/pages-ssr-bundler";
import { log } from "../../utils/log";
const { ROOT_DIR, BUNX_BUNDLER_ERROR_EXIT_FILE } = grabDirNames(); const { ROOT_DIR, BUNX_BUNDLER_ERROR_EXIT_FILE } = grabDirNames();
export default async function watcherEsbuildCTX() { export default async function watcherEsbuildCTX() {
const pages_src_watcher = watch(ROOT_DIR, { const pages_src_watcher = watch(ROOT_DIR, {
recursive: true, recursive: true,
persistent: true, persistent: true,
}, async (event, filename) => { }, async (event, filename) => {
let owns_recompile = false;
try {
if (!filename) if (!filename)
return; return;
const full_file_path = path.join(ROOT_DIR, filename);
if (global.BUNEXT_CONFIG.exclude_watch_patterns) {
for (let i = 0; i < global.BUNEXT_CONFIG.exclude_watch_patterns.length; i++) {
const watch_pattern = global.BUNEXT_CONFIG.exclude_watch_patterns[i];
if (watch_pattern instanceof RegExp) {
const is_path_excluded = watch_pattern.test(filename);
if (is_path_excluded) {
return;
}
}
else {
const excluded_path = path.resolve(ROOT_DIR, watch_pattern);
if (excluded_path == full_file_path) {
return;
}
}
}
}
if (existsSync(BUNX_BUNDLER_ERROR_EXIT_FILE)) { if (existsSync(BUNX_BUNDLER_ERROR_EXIT_FILE)) {
await fullRebuild(); await fullRebuild();
return; return;
@@ -20,23 +41,25 @@ export default async function watcherEsbuildCTX() {
if (filename.match(/^\.\w+/)) { if (filename.match(/^\.\w+/)) {
return; return;
} }
if (global.BUNDLER_CTX_DISPOSED) { if (global.BUNEXT_BUNDLER_CTX_DISPOSED) {
await fullRebuild({ msg: `Restarting Bundler ...` }); await fullRebuild({ msg: `Restarting Bundler ...` });
global.BUNDLER_CTX_DISPOSED = false; return;
} }
if (global.SSR_BUNDLER_CTX_DISPOSED) { if (global.BUNEXT_SSR_BUNDLER_CTX_DISPOSED) {
pagesSSRBundler(); await pagesSSRBundler().catch((error) => {
log.error(`SSR Bundler Error: ${error}`);
});
} }
if (filename.endsWith(AppData["BunextTmpFileExt"])) { if (filename.endsWith(AppData["BunextTmpFileExt"])) {
return; return;
} }
const full_file_path = path.join(ROOT_DIR, filename);
const does_file_exist = existsSync(full_file_path); const does_file_exist = existsSync(full_file_path);
const file_stat = does_file_exist const file_stat = does_file_exist
? statSync(full_file_path) ? statSync(full_file_path)
: undefined; : undefined;
if (full_file_path.match(/\/styles$/)) { if (full_file_path.match(/\/styles$/)) {
global.RECOMPILING = true; owns_recompile = true;
global.BUNEXT_RECOMPILING = true;
await Bun.sleep(1000); await Bun.sleep(1000);
await fullRebuild({ await fullRebuild({
msg: `Detected new \`styles\` directory. Rebuilding ...`, msg: `Detected new \`styles\` directory. Rebuilding ...`,
@@ -53,28 +76,28 @@ export default async function watcherEsbuildCTX() {
return; return;
} }
const target_files_match = /\.(tsx?|jsx?|css)$/; const target_files_match = /\.(tsx?|jsx?|css)$/;
// const rebuild_skip_paths = /\/pages\/api\//;
if (event !== "rename") { if (event !== "rename") {
if (filename.match(target_files_match)) { if (filename.match(target_files_match)) {
if (global.RECOMPILING) if (global.BUNEXT_RECOMPILING)
return; return;
global.RECOMPILING = true; owns_recompile = true;
global.BUNEXT_RECOMPILING = true;
if (filename.match(/.*\.server\.tsx?/)) { if (filename.match(/.*\.server\.tsx?/)) {
global.IS_SERVER_COMPONENT = true; global.BUNEXT_IS_SERVER_COMPONENT = true;
}
if (global.BUNDLER_CTX) {
try {
await global.BUNDLER_CTX.rebuild();
}
catch (error) {
console.log(`ESBUILD Rebuild Error =>`, error);
} }
if (global.BUNEXT_BUNDLER_CTX) {
await global.BUNEXT_BUNDLER_CTX.rebuild();
} }
if (filename.match(/(404|500)\.tsx?/)) { if (filename.match(/(404|500)\.tsx?/)) {
for (let i = global.HMR_CONTROLLERS.length - 1; i >= 0; i--) { for (let i = global.BUNEXT_HMR_CONTROLLERS.length - 1; i >= 0; i--) {
const controller = global.HMR_CONTROLLERS[i]; const controller = global.BUNEXT_HMR_CONTROLLERS[i];
try {
controller?.controller?.enqueue(`event: update\ndata: ${JSON.stringify({ reload: true })}\n\n`); controller?.controller?.enqueue(`event: update\ndata: ${JSON.stringify({ reload: true })}\n\n`);
} }
catch {
global.BUNEXT_HMR_CONTROLLERS.splice(i, 1);
}
}
} }
} }
return; return;
@@ -90,8 +113,9 @@ export default async function watcherEsbuildCTX() {
return reloadWatcher(); return reloadWatcher();
if (filename.match(/ /)) if (filename.match(/ /))
return reloadWatcher(); return reloadWatcher();
if (global.RECOMPILING) if (global.BUNEXT_RECOMPILING)
return; return;
owns_recompile = true;
const action = does_file_exist ? "created" : "deleted"; const action = does_file_exist ? "created" : "deleted";
const type = filename.match(/\.css$/) const type = filename.match(/\.css$/)
? "Sylesheet" ? "Sylesheet"
@@ -103,12 +127,22 @@ export default async function watcherEsbuildCTX() {
await fullRebuild({ await fullRebuild({
msg: `${type} ${action}: ${filename}. Rebuilding ...`, 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;
}
}
}); });
global.PAGES_SRC_WATCHER = pages_src_watcher; global.BUNEXT_PAGES_SRC_WATCHER = pages_src_watcher;
} }
function reloadWatcher() { function reloadWatcher() {
if (global.PAGES_SRC_WATCHER) { if (global.BUNEXT_PAGES_SRC_WATCHER) {
global.PAGES_SRC_WATCHER.close(); global.BUNEXT_PAGES_SRC_WATCHER.close();
watcherEsbuildCTX(); watcherEsbuildCTX();
} }
} }
+13 -6
View File
@@ -43,16 +43,18 @@ export default async function genWebHTML({ component: Main, pageProps, bundledMa
const RootHead = root_module?.Head; const RootHead = root_module?.Head;
const dev = isDevelopment(); const dev = isDevelopment();
const final_meta = _.merge(root_meta, page_meta); const final_meta = _.merge(root_meta, page_meta);
// const public_envs = Object.keys(process.env).filter((e) => const public_envs = Object.fromEntries(Object.entries(process.env).filter(([k]) => k.startsWith("BUNEXT_PUBLIC_")));
// e.startsWith(`NEXT_PUBLIC_`),
// );
const client_process = { const client_process = {
env: {}, env: {
NODE_ENV: dev ? "development" : "production",
...public_envs,
...global.BUNEXT_CONFIG.public_envs,
},
}; };
let final_component = (_jsxs("html", { ...html_props, children: [_jsxs("head", { children: [_jsx("meta", { charSet: "utf-8", "data-bunext-head": true }), _jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0", "data-bunext-head": true }), final_meta ? grabWebMetaHTML({ meta: final_meta }) : null, bundledMap?.css_path ? (_jsx("link", { rel: "stylesheet", href: `/${bundledMap.css_path}`, "data-bunext-head": true })) : null, _jsx("script", { dangerouslySetInnerHTML: { let final_component = (_jsxs("html", { ...html_props, children: [_jsxs("head", { children: [_jsx("meta", { charSet: "utf-8", "data-bunext-head": true }), _jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0", "data-bunext-head": true }), final_meta ? grabWebMetaHTML({ meta: final_meta }) : null, bundledMap?.css_path ? (_jsx("link", { rel: "stylesheet", href: `/${bundledMap.css_path}`, "data-bunext-head": true })) : null, _jsx("script", { dangerouslySetInnerHTML: {
__html: `window.${ClientWindowPagePropsName} = ${serializedProps};\nwindow.process = ${JSON.stringify(client_process)}`, __html: `window.${ClientWindowPagePropsName} = ${serializedProps};\nwindow.process = ${JSON.stringify(client_process)}`,
}, "data-bunext-head": true }), RootHead ? (_jsx(RootHead, { serverRes: pageProps, ctx: routeParams })) : null, Head ? _jsx(Head, { serverRes: pageProps, ctx: routeParams }) : null, bundledMap?.path ? (_jsxs(_Fragment, { children: [_jsx("script", { type: "importmap", dangerouslySetInnerHTML: { }, "data-bunext-head": true }), RootHead ? (_jsx(RootHead, { serverRes: pageProps, ctx: routeParams })) : null, Head ? _jsx(Head, { serverRes: pageProps, ctx: routeParams }) : null, bundledMap?.path ? (_jsxs(_Fragment, { children: [_jsx("script", { type: "importmap", dangerouslySetInnerHTML: {
__html: JSON.stringify(global.REACT_IMPORTS_MAP), __html: JSON.stringify(global.BUNEXT_REACT_IMPORTS_MAP),
}, defer: true, "data-bunext-head": true }), _jsx("script", { src: `/${bundledMap.path}`, type: "module", id: AppData["BunextClientHydrationScriptID"], defer: true, "data-bunext-head": true })] })) : null, is_dev ? (_jsx("script", { defer: true, dangerouslySetInnerHTML: { }, defer: true, "data-bunext-head": true }), _jsx("script", { src: `/${bundledMap.path}`, type: "module", id: AppData["BunextClientHydrationScriptID"], defer: true, "data-bunext-head": true })] })) : null, is_dev ? (_jsx("script", { defer: true, dangerouslySetInnerHTML: {
__html: page_hydration_script, __html: page_hydration_script,
}, "data-bunext-head": true })) : null] }), _jsx("body", { children: _jsx("div", { id: ClientRootElementIDName, suppressHydrationWarning: !dev, children: _jsx(Main, { ...pageProps }) }) })] })); }, "data-bunext-head": true })) : null] }), _jsx("body", { children: _jsx("div", { id: ClientRootElementIDName, suppressHydrationWarning: !dev, children: _jsx(Main, { ...pageProps }) }) })] }));
@@ -76,6 +78,8 @@ export default async function genWebHTML({ component: Main, pageProps, bundledMa
console.error = () => { }; console.error = () => { };
console.info = () => { }; console.info = () => { };
console.debug = () => { }; console.debug = () => { };
let htmlBody;
try {
const stream = await renderToReadableStream(final_component, { const stream = await renderToReadableStream(final_component, {
onError(error) { onError(error) {
if (error.message.includes('unique "key" prop')) if (error.message.includes('unique "key" prop'))
@@ -83,8 +87,11 @@ export default async function genWebHTML({ component: Main, pageProps, bundledMa
originalConsole.error(error); originalConsole.error(error);
}, },
}); });
const htmlBody = await new Response(stream).text(); htmlBody = await new Response(stream).text();
}
finally {
Object.assign(console, originalConsole); Object.assign(console, originalConsole);
}
html += htmlBody; html += htmlBody;
return html; return html;
} }
+1 -1
View File
@@ -24,7 +24,7 @@ export default async function grabFilePathModule({ file_path, out_file, }) {
jsx: "automatic", jsx: "automatic",
outfile: target_cache_file_path, outfile: target_cache_file_path,
}); });
Loader.registry.delete(target_cache_file_path); // Loader.registry.delete(target_cache_file_path);
const module = await import(`${target_cache_file_path}?t=${Date.now()}`); const module = await import(`${target_cache_file_path}?t=${Date.now()}`);
return module; return module;
} }
@@ -7,8 +7,10 @@ import grabDirNames from "../../../utils/grab-dir-names";
const { ROOT_DIR } = grabDirNames(); const { ROOT_DIR } = grabDirNames();
export default async function grabPageBundledReactComponent({ file_path, return_tsx_only, }) { export default async function grabPageBundledReactComponent({ file_path, return_tsx_only, }) {
try { try {
if (global.SSR_BUNDLER_CTX_MAP?.[file_path]) { if (global.BUNEXT_SSR_BUNDLER_CTX_MAP?.[file_path]) {
const mod = await import(path.join(ROOT_DIR, global.SSR_BUNDLER_CTX_MAP[file_path].path)); const abs = path.join(ROOT_DIR, global.BUNEXT_SSR_BUNDLER_CTX_MAP[file_path].path);
// Loader.registry.delete(abs);
const mod = await import(`${abs}?t=${Date.now()}`);
const Main = mod.default; const Main = mod.default;
return { component: Main }; return { component: Main };
} }
@@ -12,10 +12,13 @@ export default async function grabPageCombinedServerRes({ file_path, debug, url,
const { server_file_path: root_server_file_path } = root_file_path const { server_file_path: root_server_file_path } = root_file_path
? grabPageServerPath({ file_path: root_file_path }) ? grabPageServerPath({ file_path: root_file_path })
: {}; : {};
const root_server_ctx_map = global.SSR_BUNDLER_CTX_MAP[root_server_file_path || ""]; const root_server_ctx_map = global.BUNEXT_SSR_BUNDLER_CTX_MAP[root_server_file_path || ""];
const final_root_server_path = root_server_ctx_map?.local_path const final_root_server_path = root_server_ctx_map?.local_path
? path.join(ROOT_DIR, root_server_ctx_map.path) ? path.join(ROOT_DIR, root_server_ctx_map.path)
: root_server_file_path; : root_server_file_path;
if (final_root_server_path) {
// Loader.registry.delete(final_root_server_path);
}
const root_server_module = final_root_server_path const root_server_module = final_root_server_path
? await import(`${final_root_server_path}?t=${now}`) ? await import(`${final_root_server_path}?t=${now}`)
: undefined; : undefined;
@@ -30,10 +33,13 @@ export default async function grabPageCombinedServerRes({ file_path, debug, url,
log.info(`rootServerRes:`, rootServerRes); log.info(`rootServerRes:`, rootServerRes);
} }
const { server_file_path } = grabPageServerPath({ file_path }); const { server_file_path } = grabPageServerPath({ file_path });
const page_server_ctx = global.SSR_BUNDLER_CTX_MAP[server_file_path || ""]; const page_server_ctx = global.BUNEXT_SSR_BUNDLER_CTX_MAP[server_file_path || ""];
const final_page_server_path = page_server_ctx?.local_path const final_page_server_path = page_server_ctx?.local_path
? path.join(ROOT_DIR, page_server_ctx.path) ? path.join(ROOT_DIR, page_server_ctx.path)
: root_server_file_path; : server_file_path;
if (final_page_server_path) {
// Loader.registry.delete(final_page_server_path);
}
const server_module = final_page_server_path const server_module = final_page_server_path
? await import(`${final_page_server_path}?t=${now}`) ? await import(`${final_page_server_path}?t=${now}`)
: undefined; : undefined;
+15 -9
View File
@@ -9,6 +9,7 @@ import serverPostBuildFn from "../server-post-build-fn";
import isDevelopment from "../../../utils/is-development"; import isDevelopment from "../../../utils/is-development";
import { existsSync } from "fs"; import { existsSync } from "fs";
import grabDirNames from "../../../utils/grab-dir-names"; import grabDirNames from "../../../utils/grab-dir-names";
import watcherEsbuildCTX from "../watcher-esbuild-ctx";
const { BUNX_BUNDLER_ERROR_EXIT_FILE } = grabDirNames(); const { BUNX_BUNDLER_ERROR_EXIT_FILE } = grabDirNames();
class NotFoundError extends Error { class NotFoundError extends Error {
status = 404; status = 404;
@@ -20,7 +21,7 @@ class NotFoundError extends Error {
export default async function grabPageComponent(params) { export default async function grabPageComponent(params) {
const { req, file_path: passed_file_path, debug, return_server_res_only, skip_server_res, is_hydration, } = params; const { req, file_path: passed_file_path, debug, return_server_res_only, skip_server_res, is_hydration, } = params;
const url = req?.url ? new URL(req.url) : undefined; const url = req?.url ? new URL(req.url) : undefined;
const router = global.ROUTER; const router = global.BUNEXT_ROUTER;
const is_dev = isDevelopment(); const is_dev = isDevelopment();
const forwarded_proto = req?.headers.get("x-forwarded-proto"); const forwarded_proto = req?.headers.get("x-forwarded-proto");
if (url && forwarded_proto) { if (url && forwarded_proto) {
@@ -50,7 +51,7 @@ export default async function grabPageComponent(params) {
// log.error(errMsg); // log.error(errMsg);
throw new Error(errMsg); throw new Error(errMsg);
} }
let bundledMap = global.BUNDLER_CTX_MAP[file_path]; let bundledMap = global.BUNEXT_BUNDLER_CTX_MAP[file_path];
if (!bundledMap?.path) { if (!bundledMap?.path) {
if (does_error_file_exist) { if (does_error_file_exist) {
throw new Error(`Application Error. Please Check your components. ${match?.filePath} likely exists but has no exported module.`); throw new Error(`Application Error. Please Check your components. ${match?.filePath} likely exists but has no exported module.`);
@@ -61,7 +62,8 @@ export default async function grabPageComponent(params) {
await fullRebuild({ await fullRebuild({
msg: `Retrying Bundle map for file \`${file_path}\``, msg: `Retrying Bundle map for file \`${file_path}\``,
}); });
bundledMap = global.BUNDLER_CTX_MAP[file_path]; await Bun.sleep(1000);
bundledMap = global.BUNEXT_BUNDLER_CTX_MAP[file_path];
if (bundledMap?.path) if (bundledMap?.path)
break; break;
} }
@@ -72,7 +74,7 @@ export default async function grabPageComponent(params) {
} }
} }
if (req && !is_hydration) { if (req && !is_hydration) {
global.BUNDLER_CTX_MAP[file_path].req = req; global.BUNEXT_BUNDLER_CTX_MAP[file_path].req_url = req.url;
} }
if (debug) { if (debug) {
log.info(`bundledMap:`, bundledMap); log.info(`bundledMap:`, bundledMap);
@@ -114,8 +116,9 @@ export default async function grabPageComponent(params) {
error?.name === "NotFoundError" || error?.name === "NotFoundError" ||
error?.status === 404; error?.status === 404;
if (!params.retry && is_dev) { if (!params.retry && is_dev) {
while (global.REBUILD_RETRIES < 2) { while (global.BUNEXT_REBUILD_RETRIES < 2) {
global.REBUILD_RETRIES = global.REBUILD_RETRIES + 1; global.BUNEXT_REBUILD_RETRIES =
global.BUNEXT_REBUILD_RETRIES + 1;
await fullRebuild(); await fullRebuild();
await Bun.sleep(200); await Bun.sleep(200);
const component_retried = await grabPageComponent({ const component_retried = await grabPageComponent({
@@ -124,19 +127,22 @@ export default async function grabPageComponent(params) {
}); });
if (component_retried instanceof Response || if (component_retried instanceof Response ||
component_retried.success) { component_retried.success) {
global.REBUILD_RETRIES = 0; global.BUNEXT_REBUILD_RETRIES = 0;
await serverPostBuildFn(); await serverPostBuildFn();
return component_retried; return component_retried;
} }
} }
global.REBUILD_RETRIES = 0; global.BUNEXT_REBUILD_RETRIES = 0;
} }
if (is404) { if (is404) {
global.IS_404_PAGE = true; global.BUNEXT_IS_404_PAGE = true;
} }
else { else {
log.error(`Error Grabbing Page Component: ${error.message}`); log.error(`Error Grabbing Page Component: ${error.message}`);
log.error(`Page: ${passed_file_path || url?.pathname}`); log.error(`Page: ${passed_file_path || url?.pathname}`);
if (is_dev) {
fullRebuild();
}
} }
return await grabPageErrorComponent({ return await grabPageErrorComponent({
error, error,
@@ -2,8 +2,11 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import grabDirNames from "../../../utils/grab-dir-names"; import grabDirNames from "../../../utils/grab-dir-names";
import grabPageModules from "./grab-page-modules"; import grabPageModules from "./grab-page-modules";
import _ from "lodash"; import _ from "lodash";
import fullRebuild from "../full-rebuild";
import isDevelopment from "../../../utils/is-development";
export default async function grabPageErrorComponent({ error, routeParams, is404, url, }) { export default async function grabPageErrorComponent({ error, routeParams, is404, url, }) {
const router = global.ROUTER; const router = global.BUNEXT_ROUTER;
const is_dev = isDevelopment();
const { BUNX_ROOT_500_PRESET_COMPONENT, BUNX_ROOT_404_PRESET_COMPONENT } = grabDirNames(); const { BUNX_ROOT_500_PRESET_COMPONENT, BUNX_ROOT_404_PRESET_COMPONENT } = grabDirNames();
const errorRoute = is404 ? "/404" : "/500"; const errorRoute = is404 ? "/404" : "/500";
const presetComponent = is404 const presetComponent = is404
@@ -30,7 +33,7 @@ export default async function grabPageErrorComponent({ error, routeParams, is404
}; };
} }
const file_path = match.filePath; const file_path = match.filePath;
const bundledMap = global.BUNDLER_CTX_MAP?.[file_path]; const bundledMap = global.BUNEXT_BUNDLER_CTX_MAP?.[file_path];
const page_component = await grabPageModules({ const page_component = await grabPageModules({
file_path: file_path, file_path: file_path,
query: match?.query, query: match?.query,
@@ -51,6 +54,9 @@ export default async function grabPageErrorComponent({ error, routeParams, is404
}; };
} }
catch { catch {
if (is_dev) {
fullRebuild();
}
const DefaultNotFound = () => (_jsxs("div", { style: { const DefaultNotFound = () => (_jsxs("div", { style: {
width: "100vw", width: "100vw",
height: "100vh", height: "100vh",
+1 -1
View File
@@ -28,7 +28,7 @@ export default async function grabPageServerRes({ url, query, routeParams, serve
const serverData = await server_function({ const serverData = await server_function({
...routeParams, ...routeParams,
query: { ...routeParams.query, ...query }, query: { ...routeParams.query, ...query },
props: init_props, props: init_props || undefined,
}); });
return _.merge(default_props, serverData); return _.merge(default_props, serverData);
} }
+4 -3
View File
@@ -108,11 +108,12 @@ async function loadEntry(page_file_path) {
const now = Date.now(); const now = Date.now();
const mod_file_path = toModPath(page_file_path); const mod_file_path = toModPath(page_file_path);
const mod_css_path = mod_file_path.replace(/\.js$/, ".css"); const mod_css_path = mod_file_path.replace(/\.js$/, ".css");
if (global.REACT_DOM_MODULE_CACHE.has(page_file_path)) { if (global.BUNEXT_REACT_DOM_MODULE_CACHE.has(page_file_path)) {
return global.REACT_DOM_MODULE_CACHE.get(page_file_path)?.main; return global.BUNEXT_REACT_DOM_MODULE_CACHE.get(page_file_path)
?.main;
} }
const mod = await import(`${mod_file_path}?t=${now}`); const mod = await import(`${mod_file_path}?t=${now}`);
global.REACT_DOM_MODULE_CACHE.set(page_file_path, { global.BUNEXT_REACT_DOM_MODULE_CACHE.set(page_file_path, {
main: mod, main: mod,
css: mod_css_path, css: mod_css_path,
}); });
+13 -2
View File
@@ -66,6 +66,16 @@ export type BunextConfig = {
* bundler for the browser. Eg. `react/jsx-dev-runtime` * bundler for the browser. Eg. `react/jsx-dev-runtime`
*/ */
page_compiler_excludes?: string[]; page_compiler_excludes?: string[];
/**
* Patterns to exclude from the watcher. Eg. `./src/server.ts`
* or `\*.test.ts\`. It should either be a file path (relative or
* absolute), or a RegEx pattern.
*/
exclude_watch_patterns?: (string | RegExp)[];
/**
* Public environment variables to be passed to the client
*/
public_envs?: Record<string, string>;
}; };
export type BunextConfigMiddlewareParams = { export type BunextConfigMiddlewareParams = {
req: Request; req: Request;
@@ -203,7 +213,7 @@ export type BunextPageServerFn<T extends {
} = { } = {
[k: string]: any; [k: string]: any;
}> = (ctx: Omit<BunxRouteParams, "body"> & { }> = (ctx: Omit<BunxRouteParams, "body"> & {
props?: any; props?: T;
}) => Promise<BunextPageModuleServerReturn<T>>; }) => Promise<BunextPageModuleServerReturn<T>>;
export type BunextRouteConfig = { export type BunextRouteConfig = {
/** /**
@@ -306,7 +316,7 @@ export type BundlerCTXMap = {
url_path: string; url_path: string;
file_name: string; file_name: string;
css_path?: string; css_path?: string;
req?: Request; req_url?: string;
}; };
export type GlobalHMRControllerObject = { export type GlobalHMRControllerObject = {
controller: ReadableStreamDefaultController<string>; controller: ReadableStreamDefaultController<string>;
@@ -314,6 +324,7 @@ export type GlobalHMRControllerObject = {
target_map?: BundlerCTXMap; target_map?: BundlerCTXMap;
page_props?: any; page_props?: any;
page_reloaded?: boolean; page_reloaded?: boolean;
page_cookie?: string | null;
}; };
export type BunextCacheFileMeta = { export type BunextCacheFileMeta = {
date_created: number; date_created: number;
+2 -2
View File
@@ -1,6 +1,6 @@
export default function ({ path }) { export default function ({ path }) {
for (let i = 0; i < global.CONSTANTS.RouteIgnorePatterns.length; i++) { for (let i = 0; i < global.BUNEXT_CONSTANTS.RouteIgnorePatterns.length; i++) {
const regex = global.CONSTANTS.RouteIgnorePatterns[i]; const regex = global.BUNEXT_CONSTANTS.RouteIgnorePatterns[i];
if (path.match(regex)) if (path.match(regex))
return true; return true;
} }
-3
View File
@@ -1,6 +1,3 @@
/**
* # Convert Serialized Query back to object
*/
export default function deserializeQuery(query: string | { export default function deserializeQuery(query: string | {
[s: string]: any; [s: string]: any;
}): { }): {
+20 -5
View File
@@ -1,18 +1,33 @@
import EJSON from "./ejson"; import EJSON from "./ejson";
/** const DANGEROUS_KEYS = new Set(["__proto__", "constructor", "prototype"]);
* # Convert Serialized Query back to object function sanitize(value) {
*/ if (value === null || typeof value !== "object")
return value;
if (Array.isArray(value))
return value.map(sanitize);
const clean = Object.create(null);
for (const key of Object.keys(value)) {
if (DANGEROUS_KEYS.has(key))
continue;
clean[key] = sanitize(value[key]);
}
return clean;
}
export default function deserializeQuery(query) { export default function deserializeQuery(query) {
let queryObject = typeof query == "object" ? query : Object(EJSON.parse(query)); let queryObject = typeof query == "object" ? query : Object(EJSON.parse(query));
const keys = Object.keys(queryObject); const keys = Object.keys(queryObject);
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 value = queryObject[key]; const value = queryObject[key];
if (DANGEROUS_KEYS.has(key)) {
delete queryObject[key];
continue;
}
if (typeof value == "string") { if (typeof value == "string") {
if (value.match(/^\{|^\[/)) { if (value.match(/^\{|^\[/)) {
queryObject[key] = EJSON.parse(value); queryObject[key] = sanitize(EJSON.parse(value));
} }
} }
} }
return queryObject; return sanitize(queryObject);
} }
+6
View File
@@ -41,6 +41,12 @@ function grabPageDirRecursively({ page_dir, include_server, }) {
if (is_page_excluded) { if (is_page_excluded) {
continue; continue;
} }
if (full_page_path.match(/__tests__/)) {
continue;
}
if (page_name.match(/\.test\.(t|j)sx?/)) {
continue;
}
if (page_name.match(/\.server\.tsx?/) && !include_server) { if (page_name.match(/\.server\.tsx?/) && !include_server) {
continue; continue;
} }
+2 -2
View File
@@ -6,8 +6,8 @@ export default function grabAppPort() {
if (process.env.PORT) { if (process.env.PORT) {
return numberfy(process.env.PORT); return numberfy(process.env.PORT);
} }
if (global.CONFIG.port) { if (global.BUNEXT_CONFIG.port) {
return global.CONFIG.port; return global.BUNEXT_CONFIG.port;
} }
return numberfy(defaultPort); return numberfy(defaultPort);
} }
+2 -2
View File
@@ -1,7 +1,7 @@
import AppNames from "./grab-app-names"; import AppNames from "./grab-app-names";
export default function grabAssetsPrefix() { export default function grabAssetsPrefix() {
if (global.CONFIG.assets_prefix) { if (global.BUNEXT_CONFIG.assets_prefix) {
return global.CONFIG.assets_prefix; return global.BUNEXT_CONFIG.assets_prefix;
} }
const { defaultAssetPrefix } = AppNames; const { defaultAssetPrefix } = AppNames;
return defaultAssetPrefix; return defaultAssetPrefix;
+1 -1
View File
@@ -1,5 +1,5 @@
export default function grabConstants() { export default function grabConstants() {
const config = global.CONFIG; const config = global.BUNEXT_CONFIG;
const MB_IN_BYTES = 1024 * 1024; const MB_IN_BYTES = 1024 * 1024;
const ClientWindowPagePropsName = "__PAGE_PROPS__"; const ClientWindowPagePropsName = "__PAGE_PROPS__";
const ClientRootElementIDName = "__bunext"; const ClientRootElementIDName = "__bunext";
+2 -1
View File
@@ -1,4 +1,4 @@
export default function grabDirNames(): { export type DirNames = {
ROOT_DIR: string; ROOT_DIR: string;
SRC_DIR: string; SRC_DIR: string;
PAGES_DIR: string; PAGES_DIR: string;
@@ -27,3 +27,4 @@ export default function grabDirNames(): {
BUNX_ERROR_LOGS_DIR: string; BUNX_ERROR_LOGS_DIR: string;
BUNX_LOGS_DIR: string; BUNX_LOGS_DIR: string;
}; };
export default function grabDirNames(): DirNames;
+2
View File
@@ -1,5 +1,7 @@
import path from "path"; import path from "path";
export default function grabDirNames() { export default function grabDirNames() {
if (global.BUNEXT_DIR_NAMES)
return global.BUNEXT_DIR_NAMES;
const ROOT_DIR = process.cwd(); const ROOT_DIR = process.cwd();
const SRC_DIR = path.join(ROOT_DIR, "src"); const SRC_DIR = path.join(ROOT_DIR, "src");
const PAGES_DIR = path.join(SRC_DIR, "pages"); const PAGES_DIR = path.join(SRC_DIR, "pages");
+2 -2
View File
@@ -1,7 +1,7 @@
import grabAppPort from "./grab-app-port"; import grabAppPort from "./grab-app-port";
export default function grabOrigin() { export default function grabOrigin() {
if (global.CONFIG.origin) { if (global.BUNEXT_CONFIG.origin) {
return global.CONFIG.origin; return global.BUNEXT_CONFIG.origin;
} }
const port = grabAppPort(); const port = grabAppPort();
return `http://localhost:${port}`; return `http://localhost:${port}`;
+1 -1
View File
@@ -16,7 +16,7 @@ export default async function grabRouteParams({ req, query: passed_query, }) {
url, url,
query: _.merge(query, passed_query), query: _.merge(query, passed_query),
body, body,
server: global.SERVER, server: global.BUNEXT_SERVER,
}; };
return routeParams; return routeParams;
} }
+2 -2
View File
@@ -1,6 +1,6 @@
export default function grabRouter() { export default function grabRouter() {
// if (process.env.NODE_ENV !== "production") { // if (process.env.NODE_ENV !== "production") {
// global.ROUTER.reload(); // global.BUNEXT_ROUTER.reload();
// } // }
return global.ROUTER; return global.BUNEXT_ROUTER;
} }
+1 -5
View File
@@ -1,10 +1,6 @@
export default function isDevelopment() { export default function isDevelopment() {
const config = global.CONFIG; if (process.env.NODE_ENV === "production") {
if (process.env.NODE_ENV == "production") {
return false; return false;
} }
if (config.development) {
return true; return true;
}
return false;
} }
+4
View File
@@ -0,0 +1,4 @@
export default function isSafePath({ filePath, allowedDir, }: {
filePath: string;
allowedDir: string;
}): boolean;
+15
View File
@@ -0,0 +1,15 @@
import { realpathSync } from "fs";
import path from "path";
export default function isSafePath({ filePath, allowedDir, }) {
const resolved = path.resolve(filePath);
if (!resolved.startsWith(allowedDir + path.sep) && resolved !== allowedDir) {
return false;
}
try {
const real = realpathSync(resolved);
return (real.startsWith(allowedDir + path.sep) || real === allowedDir);
}
catch {
return false;
}
}
+1 -1
View File
@@ -21,5 +21,5 @@ export const log = {
build: (msg) => console.log(`${prefix.build} ${chalk.magenta(msg)}`), build: (msg) => console.log(`${prefix.build} ${chalk.magenta(msg)}`),
watch: (msg) => console.log(`${prefix.watch} ${chalk.blue(msg)}`), watch: (msg) => console.log(`${prefix.watch} ${chalk.blue(msg)}`),
server: (url) => console.log(`${prefix.success} ${chalk.white("Server running on")} ${chalk.cyan.underline(url)}`), server: (url) => console.log(`${prefix.success} ${chalk.white("Server running on")} ${chalk.cyan.underline(url)}`),
banner: () => console.log(`\n ${chalk.cyan.bold(AppNames.name)} ${chalk.gray(`v${global.CURRENT_VERSION || AppNames["version"]}`)}\n`), banner: () => console.log(`\n ${chalk.cyan.bold(AppNames.name)} ${chalk.gray(`v${global.BUNEXT_CURRENT_VERSION || AppNames["version"]}`)}\n`),
}; };
+1 -1
View File
@@ -5,5 +5,5 @@ export default function refreshRouter() {
style: "nextjs", style: "nextjs",
dir: PAGES_DIR, dir: PAGES_DIR,
}); });
global.ROUTER = router; global.BUNEXT_ROUTER = router;
} }
-1
View File
@@ -1 +0,0 @@
export default function registerDevPlugin(): void;
-69
View File
@@ -1,69 +0,0 @@
import { resolve, dirname, extname } from "path";
import { existsSync } from "fs";
const SOURCE_EXTENSIONS = [".tsx", ".ts", ".jsx", ".js"];
function getLoader(filePath) {
const ext = extname(filePath).slice(1);
return SOURCE_EXTENSIONS.map((e) => e.slice(1)).includes(ext) ? ext : "js";
}
function tryResolveSync(absPath) {
if (existsSync(absPath))
return absPath;
for (const ext of SOURCE_EXTENSIONS) {
const p = absPath + ext;
if (existsSync(p))
return p;
}
for (const ext of SOURCE_EXTENSIONS) {
const p = resolve(absPath, "index" + ext);
if (existsSync(p))
return p;
}
return null;
}
export default function registerDevPlugin() {
Bun.plugin({
name: "bunext-dev-hmr",
setup(build) {
// Intercept absolute-path imports that already carry ?t= (our dynamic imports)
build.onResolve({ filter: /\?t=\d+$/ }, (args) => {
if (args.path.includes("node_modules"))
return undefined;
const cleanPath = args.path.replace(/\?t=\d+$/, "");
const resolved = tryResolveSync(cleanPath);
if (!resolved)
return undefined;
if (!SOURCE_EXTENSIONS.some((e) => resolved.endsWith(e)))
return undefined;
return {
path: `${resolved}?t=${global.LAST_BUILD_TIME ?? 0}`,
namespace: "bunext-dev",
};
});
// Intercept relative imports from within bunext-dev modules
build.onResolve({ filter: /^\./ }, (args) => {
if (!/\?t=\d+/.test(args.importer))
return undefined;
// Strip "namespace:" prefix (e.g. "bunext-dev:") Bun prepends to importer
const cleanImporter = args.importer
.replace(/^[^/]+:(?=\/)/, "")
.replace(/\?t=\d+$/, "");
const base = resolve(dirname(cleanImporter), args.path);
const resolved = tryResolveSync(base);
if (!resolved)
return undefined;
if (!SOURCE_EXTENSIONS.some((e) => resolved.endsWith(e)))
return undefined;
return {
path: `${resolved}?t=${global.LAST_BUILD_TIME ?? 0}`,
namespace: "bunext-dev",
};
});
// Load files in the bunext-dev namespace from disk (async is fine in onLoad)
build.onLoad({ filter: /.*/, namespace: "bunext-dev" }, async (args) => {
const realPath = args.path.replace(/\?t=\d+$/, "");
const source = await Bun.file(realPath).text();
return { contents: source, loader: getLoader(realPath) };
});
},
});
}
+3 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@moduletrace/bunext", "name": "@moduletrace/bunext",
"version": "1.0.86", "version": "1.1.7",
"main": "dist/index.js", "main": "dist/index.js",
"module": "index.ts", "module": "index.ts",
"dependencies": { "dependencies": {
@@ -11,6 +11,7 @@
"@types/react-dom": "^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",
@@ -25,6 +26,7 @@
}, },
"devDependencies": { "devDependencies": {
"@testing-library/dom": "^10.4.1", "@testing-library/dom": "^10.4.1",
"@types/chokidar": "^2.1.7",
"@types/lodash": "^4.17.24", "@types/lodash": "^4.17.24",
"@types/micromatch": "^4.0.10", "@types/micromatch": "^4.0.10",
"happy-dom": "^20.8.4" "happy-dom": "^20.8.4"
@@ -0,0 +1,131 @@
import { describe, expect, test, beforeEach, afterEach, mock } from "bun:test";
const grabPageComponent = mock(async () => ({
serverRes: { props: { ok: true } },
}));
mock.module(
"../../../functions/server/web-pages/grab-page-component",
() => ({
default: grabPageComponent,
}),
);
const { default: serverPostBuildFn } = await import(
"../../../functions/server/server-post-build-fn"
);
function makeController(page_url: string, target_map?: { local_path: string }) {
const enqueued: string[] = [];
return {
controller: {
enqueue: (chunk: string) => {
enqueued.push(chunk);
},
} as any,
page_url,
target_map: target_map as any,
enqueued,
};
}
describe("server-post-build-fn", () => {
beforeEach(() => {
grabPageComponent.mockClear();
global.ROUTER = {
match: (path: string) => {
if (path === "/about") return { filePath: "/pages/about.tsx" };
if (path === "/404") return { filePath: "/pages/404.tsx" };
if (path === "/new-page")
return { filePath: "/pages/new-page.tsx" };
return null;
},
} as any;
global.BUNDLER_CTX_MAP = {
"/pages/about.tsx": {
local_path: "/pages/about.tsx",
path: "pages/about.js",
} as any,
"/pages/404.tsx": {
local_path: "/pages/404.tsx",
path: "pages/404.js",
} as any,
"/pages/new-page.tsx": {
local_path: "/pages/new-page.tsx",
path: "pages/new-page.js",
} as any,
};
global.HMR_CONTROLLERS = [];
global.ROOT_FILE_UPDATED = false;
});
afterEach(() => {
global.ROUTER = undefined as any;
global.BUNDLER_CTX_MAP = undefined as any;
global.HMR_CONTROLLERS = [];
});
test("soft-updates a normal page controller", async () => {
const c = makeController("http://localhost/about", {
local_path: "/pages/about.tsx",
});
global.HMR_CONTROLLERS = [c as any];
await serverPostBuildFn();
expect(c.enqueued.length).toBe(1);
expect(c.enqueued[0]).toContain("event: update");
expect(c.enqueued[0]).not.toContain('"reload":true');
expect(c.enqueued[0]).toContain("/pages/about.tsx");
});
test("soft-updates unmatched URL via custom 404 artifact", async () => {
const c = makeController("http://localhost/missing", {
local_path: "/pages/404.tsx",
});
global.HMR_CONTROLLERS = [c as any];
await serverPostBuildFn();
expect(c.enqueued.length).toBe(1);
expect(c.enqueued[0]).not.toContain('"reload":true');
expect(c.enqueued[0]).toContain("/pages/404.tsx");
expect(grabPageComponent).toHaveBeenCalled();
});
test("full-reloads when a previously unmatched route now exists", async () => {
const c = makeController("http://localhost/new-page", {
local_path: "/pages/404.tsx",
});
global.HMR_CONTROLLERS = [c as any];
await serverPostBuildFn();
expect(c.enqueued.length).toBe(1);
expect(c.enqueued[0]).toContain('"reload":true');
expect(c.target_map?.local_path).toBe("/pages/new-page.tsx");
});
test("full-reloads unmatched tab with no prior map when route appears", async () => {
const c = makeController("http://localhost/new-page");
global.HMR_CONTROLLERS = [c as any];
await serverPostBuildFn();
expect(c.enqueued.length).toBe(1);
expect(c.enqueued[0]).toContain('"reload":true');
});
test("full-reloads preset 404 tabs with no custom 404 page", async () => {
global.ROUTER = {
match: () => null,
} as any;
const c = makeController("http://localhost/missing");
global.HMR_CONTROLLERS = [c as any];
await serverPostBuildFn();
expect(c.enqueued.length).toBe(1);
expect(c.enqueued[0]).toContain('"reload":true');
});
});
+4 -12
View File
@@ -1,9 +1,8 @@
import { Command } from "commander"; import { Command } from "commander";
import { log } from "../../utils/log"; import { log } from "../../utils/log";
import init from "../../functions/init";
import grabDirNames from "../../utils/grab-dir-names"; import grabDirNames from "../../utils/grab-dir-names";
import { rmSync } from "fs"; import { rmSync } from "fs";
import allPagesESBuildContextBundler from "../../functions/bundler/all-pages-esbuild-context-bundler"; import bunextInit from "../../functions/bunext-init";
const { HYDRATION_DST_DIR, BUNX_CWD_PAGES_REWRITE_DIR } = grabDirNames(); const { HYDRATION_DST_DIR, BUNX_CWD_PAGES_REWRITE_DIR } = grabDirNames();
@@ -16,18 +15,11 @@ export default function () {
rmSync(BUNX_CWD_PAGES_REWRITE_DIR, { recursive: true }); rmSync(BUNX_CWD_PAGES_REWRITE_DIR, { recursive: true });
} catch (error) {} } catch (error) {}
global.SKIPPED_BROWSER_MODULES = new Set<string>(); global.BUNEXT_SKIPPED_BROWSER_MODULES = new Set<string>();
// await rewritePagesModule(); await bunextInit({ build_only: true });
await init();
log.banner(); log.success("Modules Built Successfully!");
log.build("Building Project ...");
// await allPagesBunBundler();
// await allPagesBundler();
await allPagesESBuildContextBundler();
process.exit(); process.exit();
}); });
+14 -3
View File
@@ -6,6 +6,14 @@ import { rmSync } from "fs";
const { HYDRATION_DST_DIR, BUNX_CWD_PAGES_REWRITE_DIR } = grabDirNames(); 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 ..."); log.info("Running development server ...");
try { try {
@@ -13,6 +21,9 @@ try {
rmSync(BUNX_CWD_PAGES_REWRITE_DIR, { recursive: true }); rmSync(BUNX_CWD_PAGES_REWRITE_DIR, { recursive: true });
} catch (error) {} } catch (error) {}
await bunextInit(); try {
await bunextInit();
await startServer(); await startServer();
} catch (error) {
log.error(`Failed to start development server: ${error}`);
}
+27 -8
View File
@@ -3,6 +3,7 @@ import path from "path";
import type { BunSpawnOptions } from "../../types"; import type { BunSpawnOptions } from "../../types";
import grabDirNames from "../../utils/grab-dir-names"; import grabDirNames from "../../utils/grab-dir-names";
import writeErrorFile from "../../functions/write-error-file"; import writeErrorFile from "../../functions/write-error-file";
import { existsSync } from "fs";
let retries = 0; let retries = 0;
let timeout: any; let timeout: any;
@@ -25,9 +26,14 @@ async function dev() {
} }
const dev_spawn_file = path.resolve(__dirname, "dev-spawn.ts"); 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: BunSpawnOptions = { const spawn_options: BunSpawnOptions = {
cmd: ["bun", dev_spawn_file], cmd: ["bun", final_spawn_file],
stdio: ["inherit", "inherit", "inherit"], stdio: ["inherit", "inherit", "inherit"],
async onExit(subprocess, exitCode, signalCode, error) { async onExit(subprocess, exitCode, signalCode, error) {
writeErrorFile({ exitCode, error }); writeErrorFile({ exitCode, error });
@@ -38,16 +44,29 @@ async function dev() {
}, },
}; };
let dev_process = Bun.spawn(spawn_options); let dev_process;
try {
dev_process = Bun.spawn(spawn_options);
} catch (error) {
console.error(`Failed to start dev process:`, error);
retries++; 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(() => { timeout = setTimeout(() => {
retries = 0; retries = 0;
}, 10000); }, 10000);
const exited = await dev_process.exited;
if (exited) {
return await dev();
}
} }
+40 -3
View File
@@ -2,6 +2,11 @@ import { Command } from "commander";
import path from "path"; import path from "path";
import type { BunSpawnOptions } from "../../types"; import type { BunSpawnOptions } from "../../types";
import writeErrorFile from "../../functions/write-error-file"; import writeErrorFile from "../../functions/write-error-file";
import { existsSync } from "fs";
let retries = 0;
let timeout: any;
const MAX_RETRIES = 5;
export default function () { export default function () {
return new Command("start") return new Command("start")
@@ -12,10 +17,24 @@ export default function () {
} }
async function start() { async function start() {
const dev_spawn_file = path.resolve(__dirname, "prod-spawn.ts"); 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: BunSpawnOptions = { const spawn_options: BunSpawnOptions = {
cmd: ["bun", dev_spawn_file], cmd: ["bun", final_spawn_file],
stdio: ["inherit", "inherit", "inherit"], stdio: ["inherit", "inherit", "inherit"],
onExit(subprocess, exitCode, signalCode, error) { onExit(subprocess, exitCode, signalCode, error) {
writeErrorFile({ exitCode, error }); writeErrorFile({ exitCode, error });
@@ -26,11 +45,29 @@ async function start() {
}, },
}; };
let dev_process = Bun.spawn(spawn_options); 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; const exited = await dev_process.exited;
if (exited) { if (exited) {
retries++;
timeout = setTimeout(() => {
retries = 0;
}, 10000);
return await start(); return await start();
} }
timeout = setTimeout(() => {
retries = 0;
}, 10000);
} }
@@ -20,6 +20,8 @@ type Params = {
post_build_fn?: (params: { post_build_fn?: (params: {
artifacts: BundlerCTXMap[]; artifacts: BundlerCTXMap[];
}) => Promise<void> | void; }) => Promise<void> | void;
build_only?: boolean;
start?: boolean;
}; };
export default async function allPagesESBuildContextBundler(params?: Params) { export default async function allPagesESBuildContextBundler(params?: Params) {
@@ -30,7 +32,7 @@ export default async function allPagesESBuildContextBundler(params?: Params) {
const pages = grabAllPages({ exclude_api: true }); const pages = grabAllPages({ exclude_api: true });
global.PAGE_FILES = pages; global.BUNEXT_PAGE_FILES = pages;
const dev = isDevelopment(); const dev = isDevelopment();
@@ -58,7 +60,7 @@ export default async function allPagesESBuildContextBundler(params?: Params) {
(e) => `hydration-virtual:${e}`, (e) => `hydration-virtual:${e}`,
); );
global.BUNDLER_CTX = await esbuild.context({ global.BUNEXT_BUNDLER_CTX = await esbuild.context({
entryPoints, entryPoints,
outdir: HYDRATION_DST_DIR, outdir: HYDRATION_DST_DIR,
bundle: true, bundle: true,
@@ -82,6 +84,7 @@ export default async function allPagesESBuildContextBundler(params?: Params) {
esbuildCTXArtifactTracker({ esbuildCTXArtifactTracker({
entryToPage, entryToPage,
post_build_fn: params?.post_build_fn, post_build_fn: params?.post_build_fn,
build_only: params?.build_only || params?.start,
}), }),
], ],
jsx: "automatic", jsx: "automatic",
@@ -93,14 +96,14 @@ export default async function allPagesESBuildContextBundler(params?: Params) {
"react-dom/client", "react-dom/client",
"react/jsx-runtime", "react/jsx-runtime",
"react/jsx-dev-runtime", "react/jsx-dev-runtime",
...(global.CONFIG.page_compiler_excludes || []), ...(global.BUNEXT_CONFIG.page_compiler_excludes || []),
], ],
logLevel: did_process_exit_because_of_bundler_error logLevel: did_process_exit_because_of_bundler_error
? "silent" ? "silent"
: undefined, : undefined,
}); });
await global.BUNDLER_CTX.rebuild(); await global.BUNEXT_BUNDLER_CTX.rebuild();
} catch (error) { } catch (error) {
console.log(`ESBUILD Error =>`, error); console.log(`ESBUILD Error =>`, error);
} }
@@ -4,16 +4,23 @@ export default async function buildOnstartErrorHandler(params?: Params) {
// const error_msg = `Build Failed. Please check all your components and imports.`; // const error_msg = `Build Failed. Please check all your components and imports.`;
// log.error(error_msg); // log.error(error_msg);
global.BUNDLER_CTX_DISPOSED = true; if (global.BUNEXT_BUNDLER_CTX_DISPOSED) {
return;
}
global.RECOMPILING = false; // console.log(`Killing Bundler ...`);
global.IS_SERVER_COMPONENT = false; // console.log(`global.BUNEXT_BUNDLER_CTX_DISPOSED`, global.BUNEXT_BUNDLER_CTX_DISPOSED);
Promise.all([ global.BUNEXT_BUNDLER_CTX_DISPOSED = true;
global.SSR_BUNDLER_CTX?.dispose(),
global.BUNDLER_CTX?.dispose(), 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.SSR_BUNDLER_CTX = undefined; global.BUNEXT_SSR_BUNDLER_CTX = undefined;
global.BUNDLER_CTX = undefined; global.BUNEXT_BUNDLER_CTX = undefined;
} }
@@ -74,7 +74,7 @@ export default async function bunReactModulesBundler() {
const PUBLIC_ROOT = BUNEXT_VENDOR_DIR.replace(BUNX_CWD_DIR, "/.bunext"); const PUBLIC_ROOT = BUNEXT_VENDOR_DIR.replace(BUNX_CWD_DIR, "/.bunext");
global.REACT_IMPORTS_MAP = { global.BUNEXT_REACT_IMPORTS_MAP = {
imports: { imports: {
react: `${PUBLIC_ROOT}/react.js`, react: `${PUBLIC_ROOT}/react.js`,
"react-dom": `${PUBLIC_ROOT}/react-dom.js`, "react-dom": `${PUBLIC_ROOT}/react-dom.js`,
+7 -2
View File
@@ -23,7 +23,7 @@ export default async function pagesSSRBundler(params?: Params) {
include_server: true, include_server: true,
}); });
const dev = isDevelopment(); const dev = isDevelopment();
const config = global.CONFIG; const config = global.BUNEXT_CONFIG;
try { try {
writeFileSync( writeFileSync(
@@ -75,6 +75,7 @@ export default async function pagesSSRBundler(params?: Params) {
); );
} catch (error) {} } catch (error) {}
try {
await esbuild.build({ await esbuild.build({
entryPoints, entryPoints,
outdir: BUNX_CWD_MODULE_CACHE_DIR, outdir: BUNX_CWD_MODULE_CACHE_DIR,
@@ -107,11 +108,15 @@ export default async function pagesSSRBundler(params?: Params) {
"react/jsx-runtime", "react/jsx-runtime",
"react/jsx-dev-runtime", "react/jsx-dev-runtime",
"bun:*", "bun:*",
"bun",
"sqlite-vec", "sqlite-vec",
"better-sqlite3", "better-sqlite3",
...(config.ssr_compiler_excludes || []), ...(config.ssr_compiler_excludes || []),
], ],
splitting: true, splitting: true,
// logLevel: "silent",
}); });
} catch (error) {
global.BUNEXT_SSR_BUNDLER_CTX_DISPOSED = true;
log.error(`SSR Bundler Error: ${error}`);
}
} }
@@ -19,9 +19,9 @@ export default async function pagesSSRContextBundler(params?: Params) {
const pages = grabAllPages(); const pages = grabAllPages();
const dev = isDevelopment(); const dev = isDevelopment();
if (global.SSR_BUNDLER_CTX) { if (global.BUNEXT_SSR_BUNDLER_CTX) {
await global.SSR_BUNDLER_CTX.dispose(); await global.BUNEXT_SSR_BUNDLER_CTX.dispose();
global.SSR_BUNDLER_CTX = undefined; global.BUNEXT_SSR_BUNDLER_CTX = undefined;
} }
const entryToPage = new Map<string, PageFiles & { tsx: string }>(); const entryToPage = new Map<string, PageFiles & { tsx: string }>();
@@ -46,7 +46,7 @@ export default async function pagesSSRContextBundler(params?: Params) {
const entryPoints = [...entryToPage.keys()].map((e) => `ssr-virtual:${e}`); const entryPoints = [...entryToPage.keys()].map((e) => `ssr-virtual:${e}`);
global.SSR_BUNDLER_CTX = await esbuild.context({ global.BUNEXT_SSR_BUNDLER_CTX = await esbuild.context({
entryPoints, entryPoints,
outdir: BUNX_CWD_MODULE_CACHE_DIR, outdir: BUNX_CWD_MODULE_CACHE_DIR,
bundle: true, bundle: true,
@@ -82,5 +82,5 @@ export default async function pagesSSRContextBundler(params?: Params) {
// logLevel: "silent", // logLevel: "silent",
}); });
await global.SSR_BUNDLER_CTX.rebuild(); await global.BUNEXT_SSR_BUNDLER_CTX.rebuild();
} }
@@ -9,7 +9,7 @@ const BunSkipNonBrowserPlugin: Bun.BunPlugin = {
// const skipped_modules = new Set<string>(); // const skipped_modules = new Set<string>();
build.onResolve({ filter: skipFilter }, (args) => { build.onResolve({ filter: skipFilter }, (args) => {
global.SKIPPED_BROWSER_MODULES.add(args.path); global.BUNEXT_SKIPPED_BROWSER_MODULES.add(args.path);
return { return {
path: args.path, path: args.path,
namespace: "skipped", namespace: "skipped",
@@ -18,8 +18,8 @@ const BunSkipNonBrowserPlugin: Bun.BunPlugin = {
}); });
// build.onEnd(() => { // build.onEnd(() => {
// log.warn(`global.SKIPPED_BROWSER_MODULES`, [ // log.warn(`global.BUNEXT_SKIPPED_BROWSER_MODULES`, [
// ...global.SKIPPED_BROWSER_MODULES, // ...global.BUNEXT_SKIPPED_BROWSER_MODULES,
// ]); // ]);
// }); // });
@@ -14,7 +14,6 @@ import cleanupLogsDirs from "../../cleanup-logs-dir";
const { BUNX_BUNDLER_ERROR_EXIT_FILE, BUNX_ERROR_LOGS_DIR } = grabDirNames(); const { BUNX_BUNDLER_ERROR_EXIT_FILE, BUNX_ERROR_LOGS_DIR } = grabDirNames();
let build_start = 0; let build_start = 0;
let build_starts = 0;
const MAX_BUILD_STARTS = 2; const MAX_BUILD_STARTS = 2;
type Params = { type Params = {
@@ -25,17 +24,19 @@ type Params = {
} }
>; >;
post_build_fn?: (params: { artifacts: any[] }) => Promise<void> | void; post_build_fn?: (params: { artifacts: any[] }) => Promise<void> | void;
build_only?: boolean;
}; };
export default function esbuildCTXArtifactTracker({ export default function esbuildCTXArtifactTracker({
entryToPage, entryToPage,
post_build_fn, post_build_fn,
build_only,
}: Params) { }: Params) {
const artifactTracker: Plugin = { const artifactTracker: Plugin = {
name: "artifact-tracker", name: "artifact-tracker",
setup(build) { setup(build) {
build.onStart(async () => { build.onStart(async () => {
build_starts++; global.BUNEXT_MAIN_CTX_BUILD_STARTS++;
build_start = performance.now(); build_start = performance.now();
const does_error_file_exist = existsSync( const does_error_file_exist = existsSync(
@@ -43,32 +44,37 @@ export default function esbuildCTXArtifactTracker({
); );
if ( if (
build_starts >= MAX_BUILD_STARTS && global.BUNEXT_MAIN_CTX_BUILD_STARTS >= MAX_BUILD_STARTS &&
!does_error_file_exist !does_error_file_exist
) { ) {
await buildOnstartErrorHandler(); await buildOnstartErrorHandler();
} }
}); });
build.onEnd((result) => { build.onEnd(async (result) => {
if (result.errors.length > 0) { if (result.errors.length > 0) {
global.RECOMPILING = false; global.BUNEXT_RECOMPILING = false;
global.IS_SERVER_COMPONENT = false; global.BUNEXT_IS_SERVER_COMPONENT = false;
build_starts = 0;
log.error(`Build errors:`); log.error(`Build errors:`);
for (const err of result.errors) { for (const err of result.errors) {
log.error(` ${err.text}${err.location ? ` (${err.location.file}:${err.location.line}:${err.location.column})` : ""}`); log.error(
` ${err.text}${err.location ? ` (${err.location.file}:${err.location.line}:${err.location.column})` : ""}`,
);
} }
for (let i = global.HMR_CONTROLLERS.length - 1; i >= 0; i--) { for (
const controller = global.HMR_CONTROLLERS[i]; let i = global.BUNEXT_HMR_CONTROLLERS.length - 1;
i >= 0;
i--
) {
const controller = global.BUNEXT_HMR_CONTROLLERS[i];
try { try {
controller?.controller?.enqueue( controller?.controller?.enqueue(
`event: update\ndata: ${JSON.stringify({ reload: true })}\n\n`, `event: update\ndata: ${JSON.stringify({ reload: true })}\n\n`,
); );
} catch { } catch {
global.HMR_CONTROLLERS.splice(i, 1); global.BUNEXT_HMR_CONTROLLERS.splice(i, 1);
} }
} }
@@ -83,31 +89,39 @@ export default function esbuildCTXArtifactTracker({
if (artifacts?.[0] && artifacts.length > 0) { if (artifacts?.[0] && artifacts.length > 0) {
for (let i = 0; i < artifacts.length; i++) { for (let i = 0; i < artifacts.length; i++) {
const artifact = artifacts[i]; const artifact = artifacts[i];
if (artifact?.local_path && global.BUNDLER_CTX_MAP) { if (
global.BUNDLER_CTX_MAP[artifact.local_path] = artifact?.local_path &&
global.BUNEXT_BUNDLER_CTX_MAP
) {
global.BUNEXT_BUNDLER_CTX_MAP[artifact.local_path] =
_.merge( _.merge(
global.BUNDLER_CTX_MAP[artifact.local_path], global.BUNEXT_BUNDLER_CTX_MAP[
artifact.local_path
],
artifact, artifact,
); );
} }
} }
post_build_fn?.({ artifacts });
} }
const elapsed = (performance.now() - build_start).toFixed(0); const elapsed = (performance.now() - build_start).toFixed(0);
log.success(`[Built] in ${elapsed}ms`); log.success(`[Built] in ${elapsed}ms`);
global.RECOMPILING = false; global.BUNEXT_MAIN_CTX_BUILD_STARTS = 0;
global.IS_SERVER_COMPONENT = false; global.BUNEXT_BUNDLER_CTX_DISPOSED = false;
build_starts = 0;
const does_error_file_exist = existsSync( const does_error_file_exist = existsSync(
BUNX_BUNDLER_ERROR_EXIT_FILE, BUNX_BUNDLER_ERROR_EXIT_FILE,
); );
if (does_error_file_exist) { // 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 }); mkdirSync(BUNX_ERROR_LOGS_DIR, { recursive: true });
cpSync( cpSync(
BUNX_BUNDLER_ERROR_EXIT_FILE, BUNX_BUNDLER_ERROR_EXIT_FILE,
@@ -115,20 +129,25 @@ export default function esbuildCTXArtifactTracker({
); );
rmSync(BUNX_BUNDLER_ERROR_EXIT_FILE, { force: true }); rmSync(BUNX_BUNDLER_ERROR_EXIT_FILE, { force: true });
cleanupLogsDirs(); cleanupLogsDirs();
fullRebuild(); await fullRebuild();
} else { } else {
try { try {
pagesSSRBundler(); await pagesSSRBundler();
} catch (error) { } catch (error) {
log.error(`SSR Bundler Error: ${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}`);
}
}
} }
// if (global.SSR_BUNDLER_CTX) { global.BUNEXT_RECOMPILING = false;
// global.SSR_BUNDLER_CTX.rebuild(); global.BUNEXT_IS_SERVER_COMPONENT = false;
// } else {
// pagesSSRContextBundler();
// }
}); });
}, },
}; };
@@ -32,17 +32,25 @@ export default function ssrCTXArtifactTracker({
build_starts++; build_starts++;
build_start = performance.now(); build_start = performance.now();
if (build_starts == MAX_BUILD_STARTS) { if (build_starts == MAX_BUILD_STARTS) {
global.SSR_BUNDLER_CTX_DISPOSED = true; global.BUNEXT_SSR_BUNDLER_CTX_DISPOSED = true;
await global.SSR_BUNDLER_CTX?.dispose(); await global.BUNEXT_SSR_BUNDLER_CTX?.dispose();
global.SSR_BUNDLER_CTX = undefined; global.BUNEXT_SSR_BUNDLER_CTX = undefined;
} }
}); });
build.onEnd((result) => { build.onEnd(async (result) => {
if (result.errors.length > 0) { if (result.errors.length > 0) {
global.SSR_BUNDLER_CTX_DISPOSED = false; 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; build_starts = 0;
console.log("SSR Build errors:", result.errors); 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; return;
} }
@@ -57,10 +65,11 @@ export default function ssrCTXArtifactTracker({
const artifact = artifacts[i]; const artifact = artifacts[i];
if ( if (
artifact?.local_path && artifact?.local_path &&
global.SSR_BUNDLER_CTX_MAP global.BUNEXT_SSR_BUNDLER_CTX_MAP
) { ) {
global.SSR_BUNDLER_CTX_MAP[artifact.local_path] = global.BUNEXT_SSR_BUNDLER_CTX_MAP[
artifact; artifact.local_path
] = artifact;
} }
} }
@@ -75,11 +84,15 @@ export default function ssrCTXArtifactTracker({
try { try {
writeFileSync( writeFileSync(
path.join(BUNX_TMP_DIR, "ctx-map.json"), path.join(BUNX_TMP_DIR, "ctx-map.json"),
JSON.stringify(global.SSR_BUNDLER_CTX_MAP, null, 4), JSON.stringify(
global.BUNEXT_SSR_BUNDLER_CTX_MAP,
null,
4,
),
); );
} catch (error) {} } catch (error) {}
global.SSR_BUNDLER_CTX_DISPOSED = false; global.BUNEXT_SSR_BUNDLER_CTX_DISPOSED = false;
}); });
}, },
}; };
@@ -1,7 +1,6 @@
import type { Plugin } from "esbuild"; import type { Plugin } from "esbuild";
import path from "path"; import path from "path";
import type { PageFiles } from "../../../types"; import type { PageFiles } from "../../../types";
import { log } from "../../../utils/log";
type Params = { type Params = {
entryToPage: Map< entryToPage: Map<
@@ -77,7 +77,7 @@ export default async function reactModulesBundler() {
const PUBLIC_ROOT = BUNEXT_VENDOR_DIR.replace(BUNX_CWD_DIR, "/.bunext"); const PUBLIC_ROOT = BUNEXT_VENDOR_DIR.replace(BUNX_CWD_DIR, "/.bunext");
global.REACT_IMPORTS_MAP = { global.BUNEXT_REACT_IMPORTS_MAP = {
imports: { imports: {
react: `${PUBLIC_ROOT}/react.js`, react: `${PUBLIC_ROOT}/react.js`,
"react-dom": `${PUBLIC_ROOT}/react-dom.js`, "react-dom": `${PUBLIC_ROOT}/react-dom.js`,
+5 -2
View File
@@ -21,8 +21,11 @@ export default async function recordArtifacts({
} }
} }
if (global.BUNDLER_CTX_MAP) { if (global.BUNEXT_BUNDLER_CTX_MAP) {
global.BUNDLER_CTX_MAP = _.merge(global.BUNDLER_CTX_MAP, artifacts_map); global.BUNEXT_BUNDLER_CTX_MAP = _.merge(
global.BUNEXT_BUNDLER_CTX_MAP,
artifacts_map,
);
} }
// await Bun.write( // await Bun.write(
+60 -52
View File
@@ -5,76 +5,81 @@ import type {
PageFiles, PageFiles,
} from "../types"; } from "../types";
import type { FileSystemRouter, Server } from "bun"; import type { FileSystemRouter, Server } from "bun";
import grabDirNames from "../utils/grab-dir-names"; import grabDirNames, { type DirNames } from "../utils/grab-dir-names";
import { type FSWatcher } from "fs";
import init from "./init"; import init from "./init";
import isDevelopment from "../utils/is-development"; import isDevelopment from "../utils/is-development";
import { log } from "../utils/log"; import { log } from "../utils/log";
import cron from "./server/cron"; import cron from "./server/cron";
import type { BuildContext } from "esbuild"; import type { BuildContext } from "esbuild";
import watcherEsbuildCTX from "./server/watcher-esbuild-ctx";
import allPagesESBuildContextBundler from "./bundler/all-pages-esbuild-context-bundler"; import allPagesESBuildContextBundler from "./bundler/all-pages-esbuild-context-bundler";
import serverPostBuildFn from "./server/server-post-build-fn"; import serverPostBuildFn from "./server/server-post-build-fn";
import reactModulesBundler from "./bundler/react-modules-bundler"; import reactModulesBundler from "./bundler/react-modules-bundler";
import grabConstants from "../utils/grab-constants"; import grabConstants from "../utils/grab-constants";
import watcherEsbuildCTX from "./server/watcher-esbuild-ctx";
import type { FSWatcher } from "fs";
/** /**
* # Declare Global Variables * # Declare Global Variables
*/ */
declare global { declare global {
var CONFIG: BunextConfig; var BUNEXT_CONFIG: BunextConfig;
var SERVER: Server<any> | undefined; var BUNEXT_SERVER: Server<any> | undefined;
var RECOMPILING: boolean; var BUNEXT_RECOMPILING: boolean;
var BUILDING_SSR: boolean; var BUNEXT_BUILDING_SSR: boolean;
var IS_SERVER_COMPONENT: boolean; var BUNEXT_IS_SERVER_COMPONENT: boolean;
var WATCHER_TIMEOUT: any; var BUNEXT_WATCHER_TIMEOUT: any;
var ROUTER: FileSystemRouter; var BUNEXT_ROUTER: FileSystemRouter;
var HMR_CONTROLLERS: GlobalHMRControllerObject[]; var BUNEXT_HMR_CONTROLLERS: GlobalHMRControllerObject[];
var LAST_BUILD_TIME: number; var BUNEXT_LAST_BUILD_TIME: number;
var BUNDLER_CTX_MAP: { [k: string]: BundlerCTXMap }; var BUNEXT_BUNDLER_CTX_MAP: { [k: string]: BundlerCTXMap };
var SSR_BUNDLER_CTX_MAP: { [k: string]: BundlerCTXMap }; var BUNEXT_SSR_BUNDLER_CTX_MAP: { [k: string]: BundlerCTXMap };
// var API_ROUTES_BUNDLER_CTX_MAP: { [k: string]: BundlerCTXMap }; // var BUNEXT_API_ROUTES_BUNDLER_CTX_MAP: { [k: string]: BundlerCTXMap };
var BUNDLER_REBUILDS: 0; var BUNEXT_BUNDLER_REBUILDS: 0;
var PAGES_SRC_WATCHER: FSWatcher | undefined; var BUNEXT_PAGES_SRC_WATCHER: FSWatcher | undefined;
var CURRENT_VERSION: string | undefined; var BUNEXT_CURRENT_VERSION: string | undefined;
var PAGE_FILES: PageFiles[]; var BUNEXT_PAGE_FILES: PageFiles[];
var ROOT_FILE_UPDATED: boolean; var BUNEXT_ROOT_FILE_UPDATED: boolean;
var SKIPPED_BROWSER_MODULES: Set<string>; var BUNEXT_SKIPPED_BROWSER_MODULES: Set<string>;
var BUNDLER_CTX: BuildContext | undefined; var BUNEXT_BUNDLER_CTX: BuildContext | undefined;
var SSR_BUNDLER_CTX: BuildContext | undefined; var BUNEXT_SSR_BUNDLER_CTX: BuildContext | undefined;
// var API_ROUTES_BUNDLER_CTX: BuildContext | undefined; // var BUNEXT_API_ROUTES_BUNDLER_CTX: BuildContext | undefined;
var DIR_NAMES: ReturnType<typeof grabDirNames>; var BUNEXT_DIR_NAMES: DirNames;
var REACT_IMPORTS_MAP: { imports: Record<string, string> }; var BUNEXT_REACT_IMPORTS_MAP: { imports: Record<string, string> };
var REACT_DOM_SERVER: any; var BUNEXT_REACT_DOM_SERVER: any;
var REACT_DOM_MODULE_CACHE: Map<string, { main: any; css: string }>; var BUNEXT_REACT_DOM_MODULE_CACHE: Map<string, { main: any; css: string }>;
var BUNDLER_CTX_DISPOSED: boolean | undefined; var BUNEXT_BUNDLER_CTX_DISPOSED: boolean | undefined;
var SSR_BUNDLER_CTX_DISPOSED: boolean | undefined; var BUNEXT_SSR_BUNDLER_CTX_DISPOSED: boolean | undefined;
var REBUILD_RETRIES: number; var BUNEXT_REBUILD_RETRIES: number;
var IS_404_PAGE: boolean; var BUNEXT_IS_404_PAGE: boolean;
var CONSTANTS: ReturnType<typeof grabConstants>; var BUNEXT_CONSTANTS: ReturnType<typeof grabConstants>;
var BUNEXT_MAIN_CTX_BUILD_STARTS: number;
} }
const dirNames = grabDirNames(); const dirNames = grabDirNames();
const { PAGES_DIR } = dirNames; const { PAGES_DIR } = dirNames;
export default async function bunextInit() { type Params = {
global.HMR_CONTROLLERS = []; build_only?: boolean;
global.BUNDLER_CTX_MAP = {}; };
global.SSR_BUNDLER_CTX_MAP = {};
// global.API_ROUTES_BUNDLER_CTX_MAP = {};
global.BUNDLER_REBUILDS = 0;
global.REBUILD_RETRIES = 0;
global.PAGE_FILES = [];
global.SKIPPED_BROWSER_MODULES = new Set<string>();
global.DIR_NAMES = dirNames;
global.REACT_IMPORTS_MAP = { imports: {} };
global.REACT_DOM_MODULE_CACHE = new Map<string, any>();
log.banner(); export default async function bunextInit(params?: 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<string>();
global.BUNEXT_DIR_NAMES = dirNames;
global.BUNEXT_REACT_IMPORTS_MAP = { imports: {} };
global.BUNEXT_REACT_DOM_MODULE_CACHE = new Map<string, any>();
global.BUNEXT_MAIN_CTX_BUILD_STARTS = 0;
await init(); await init();
log.banner();
global.CONSTANTS = grabConstants(); global.BUNEXT_CONSTANTS = grabConstants();
await reactModulesBundler(); await reactModulesBundler();
@@ -83,21 +88,24 @@ export default async function bunextInit() {
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) {
log.build(`Building Modules ...`);
await allPagesESBuildContextBundler();
} else if (is_dev) {
log.build(`Building Modules ...`); log.build(`Building Modules ...`);
await allPagesESBuildContextBundler({ await allPagesESBuildContextBundler({
post_build_fn: () => { post_build_fn: async () => {
serverPostBuildFn(); await serverPostBuildFn();
}, },
}); });
watcherEsbuildCTX(); watcherEsbuildCTX();
} else { } else {
log.build(`Building Modules ...`); log.build(`Building Modules ...`);
await allPagesESBuildContextBundler(); await allPagesESBuildContextBundler({ start: true });
cron(); cron();
} }
} }
+5
View File
@@ -17,6 +17,11 @@ 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) {
return undefined; return undefined;
+1 -1
View File
@@ -18,7 +18,7 @@ export default async function trimCacheKey({
key, key,
}); });
const config = global.CONFIG; const config = global.BUNEXT_CONFIG;
const default_expiry_time_seconds = const default_expiry_time_seconds =
config.default_cache_expiry || config.default_cache_expiry ||
+2 -2
View File
@@ -35,7 +35,7 @@ export default async function () {
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) as (keyof ReturnType< const keys = Object.keys(dirNames) as (keyof ReturnType<
@@ -65,7 +65,7 @@ export default async function () {
const config: BunextConfig = (await grabConfig()) || {}; const config: BunextConfig = (await grabConfig()) || {};
global.CONFIG = { global.BUNEXT_CONFIG = {
...config, ...config,
development: is_dev, development: is_dev,
}; };
+16 -2
View File
@@ -8,6 +8,10 @@ import handleBunextPublicAssets from "./handle-bunext-public-assets";
import checkExcludedPatterns from "../../utils/check-excluded-patterns"; import checkExcludedPatterns from "../../utils/check-excluded-patterns";
import { AppData } from "../../data/app-data"; import { AppData } from "../../data/app-data";
import fullRebuild from "./full-rebuild"; import fullRebuild from "./full-rebuild";
const HMR_RETRY_COOLDOWN_MS = 5000;
let lastHmrRetryTime = 0;
type Params = { type Params = {
req: Request; req: Request;
server: Bun.Server<any>; server: Bun.Server<any>;
@@ -29,8 +33,9 @@ export default async function bunextRequestHandler({
let response: Response | undefined = undefined; let response: Response | undefined = undefined;
if (global.CONSTANTS.config?.middleware) { if (global.BUNEXT_CONSTANTS.config?.middleware) {
const middleware_res = await global.CONSTANTS.config.middleware({ const middleware_res =
await global.BUNEXT_CONSTANTS.config.middleware({
req: initial_req, req: initial_req,
url, url,
}); });
@@ -45,6 +50,11 @@ export default async function bunextRequestHandler({
} }
if (is_dev && url.pathname == AppData["BunextHMRRetryRoute"]) { if (is_dev && url.pathname == AppData["BunextHMRRetryRoute"]) {
const now = Date.now();
if (now - lastHmrRetryTime < HMR_RETRY_COOLDOWN_MS) {
return new Response("Too Many Requests", { status: 429 });
}
lastHmrRetryTime = now;
await fullRebuild({ msg: `HMR Retry Rebuild ...` }); await fullRebuild({ msg: `HMR Retry Rebuild ...` });
return new Response("Modules Rebuilt"); return new Response("Modules Rebuilt");
} }
@@ -76,8 +86,12 @@ export default async function bunextRequestHandler({
return response; return response;
} catch (error: any) { } catch (error: any) {
if (is_dev) {
return new Response(`Server Error: ${error.message}`, { return new Response(`Server Error: ${error.message}`, {
status: 500, status: 500,
}); });
} }
console.error(`Server Error: ${error.message}`, error);
return new Response("Internal Server Error", { status: 500 });
}
} }
@@ -0,0 +1,161 @@
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: string) => path.endsWith(AppData["BunextTmpFileExt"]),
],
persistent: true,
ignoreInitial: true,
depth: 99,
});
const handleEvent = async (
event: "add" | "change" | "unlink" | "addDir" | "unlinkDir",
filePath: string,
) => {
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();
}
}
+16 -16
View File
@@ -1,6 +1,5 @@
import { log } from "../../utils/log"; import { log } from "../../utils/log";
import allPagesESBuildContextBundler from "../bundler/all-pages-esbuild-context-bundler"; import allPagesESBuildContextBundler from "../bundler/all-pages-esbuild-context-bundler";
import pagesSSRBundler from "../bundler/pages-ssr-bundler";
import serverPostBuildFn from "./server-post-build-fn"; import serverPostBuildFn from "./server-post-build-fn";
import watcherEsbuildCTX from "./watcher-esbuild-ctx"; import watcherEsbuildCTX from "./watcher-esbuild-ctx";
@@ -8,35 +7,36 @@ export default async function fullRebuild(params?: { msg?: string }) {
try { try {
const { msg } = params || {}; const { msg } = params || {};
global.RECOMPILING = true; global.BUNEXT_RECOMPILING = true;
if (msg) { if (msg) {
log.watch(msg); log.watch(msg);
} }
global.ROUTER.reload(); global.BUNEXT_ROUTER.reload();
await global.BUNDLER_CTX?.dispose(); try {
global.BUNDLER_CTX = undefined; await global.BUNEXT_BUNDLER_CTX?.dispose();
global.BUNEXT_BUNDLER_CTX = undefined;
await global.SSR_BUNDLER_CTX?.dispose(); await global.BUNEXT_SSR_BUNDLER_CTX?.dispose();
global.SSR_BUNDLER_CTX = undefined; global.BUNEXT_SSR_BUNDLER_CTX = undefined;
} catch (error) {}
await pagesSSRBundler(); await allPagesESBuildContextBundler({
post_build_fn: async () => {
allPagesESBuildContextBundler({ await serverPostBuildFn();
post_build_fn: () => {
serverPostBuildFn();
}, },
}); });
} catch (error: any) { } catch (error: any) {
global.RECOMPILING = false;
global.IS_SERVER_COMPONENT = false;
log.error(error); log.error(error);
} finally {
global.BUNEXT_RECOMPILING = false;
global.BUNEXT_IS_SERVER_COMPONENT = false;
} }
if (global.PAGES_SRC_WATCHER) { if (global.BUNEXT_PAGES_SRC_WATCHER) {
global.PAGES_SRC_WATCHER.close(); global.BUNEXT_PAGES_SRC_WATCHER.close();
watcherEsbuildCTX(); watcherEsbuildCTX();
} }
} }
@@ -2,6 +2,7 @@ import grabDirNames from "../../utils/grab-dir-names";
import path from "path"; import path from "path";
import isDevelopment from "../../utils/is-development"; import isDevelopment from "../../utils/is-development";
import { readFileResponse } from "./handle-public"; import { readFileResponse } from "./handle-public";
import isSafePath from "../../utils/is-safe-path";
const { BUNEXT_PUBLIC_DIR } = grabDirNames(); const { BUNEXT_PUBLIC_DIR } = grabDirNames();
@@ -19,7 +20,7 @@ export default async function ({ req }: Params): Promise<Response> {
url.pathname.replace(/\/\.bunext\/public\//, ""), url.pathname.replace(/\/\.bunext\/public\//, ""),
); );
if (!file_path.startsWith(BUNEXT_PUBLIC_DIR + path.sep)) { if (!isSafePath({ filePath: file_path, allowedDir: BUNEXT_PUBLIC_DIR })) {
return new Response("Forbidden", { status: 403 }); return new Response("Forbidden", { status: 403 });
} }
+9 -2
View File
@@ -2,6 +2,7 @@ import grabDirNames from "../../utils/grab-dir-names";
import path from "path"; import path from "path";
import isDevelopment from "../../utils/is-development"; import isDevelopment from "../../utils/is-development";
import { existsSync } from "fs"; import { existsSync } from "fs";
import isSafePath from "../../utils/is-safe-path";
const { PUBLIC_DIR } = grabDirNames(); const { PUBLIC_DIR } = grabDirNames();
@@ -15,7 +16,7 @@ export default async function ({ req }: Params): Promise<Response> {
const url = new URL(req.url); const url = new URL(req.url);
const file_path = path.join(PUBLIC_DIR, url.pathname); const file_path = path.join(PUBLIC_DIR, url.pathname);
if (!file_path.startsWith(PUBLIC_DIR + path.sep)) { if (!isSafePath({ filePath: file_path, allowedDir: PUBLIC_DIR })) {
return new Response("Forbidden", { status: 403 }); return new Response("Forbidden", { status: 403 });
} }
@@ -26,7 +27,13 @@ export default async function ({ req }: Params): Promise<Response> {
} }
const file = Bun.file(file_path); const file = Bun.file(file_path);
return new Response(file); const headers = new Headers();
if (!is_dev) {
headers.set("Cache-Control", "public, max-age=3600");
}
return new Response(file, { headers });
} catch (error) { } catch (error) {
return new Response(`File Not Found`, { return new Response(`File Not Found`, {
status: 404, status: 404,
+29 -14
View File
@@ -2,12 +2,34 @@ type Params = {
req: Request; req: Request;
}; };
function removeController(controller: ReadableStreamDefaultController<string>) {
const idx = global.BUNEXT_HMR_CONTROLLERS.findIndex(
(c) => c.controller == controller,
);
if (typeof idx == "number" && idx >= 0) {
global.BUNEXT_HMR_CONTROLLERS.splice(idx, 1);
}
}
export default async function ({ req }: Params): Promise<Response> { export default async function ({ req }: Params): Promise<Response> {
const referer_url = new URL(req.headers.get("referer") || ""); const referer = req.headers.get("referer");
const match = global.ROUTER.match(referer_url.pathname); const page_cookie = req.headers.get("cookie");
if (!referer) {
return new Response("Missing Referer Header", { status: 400 });
}
let referer_url: URL;
try {
referer_url = new URL(referer);
} catch {
return new Response("Invalid Referer Header", { status: 400 });
}
const match = global.BUNEXT_ROUTER.match(referer_url.pathname);
const target_map = match?.filePath const target_map = match?.filePath
? global.BUNDLER_CTX_MAP?.[match.filePath] ? global.BUNEXT_BUNDLER_CTX_MAP?.[match.filePath]
: undefined; : undefined;
let controller: ReadableStreamDefaultController<string>; let controller: ReadableStreamDefaultController<string>;
@@ -15,31 +37,24 @@ export default async function ({ req }: Params): Promise<Response> {
const stream = new ReadableStream<string>({ const stream = new ReadableStream<string>({
start(c) { start(c) {
controller = c; controller = c;
global.HMR_CONTROLLERS.push({ global.BUNEXT_HMR_CONTROLLERS.push({
controller: c, controller: c,
page_url: referer_url.href, page_url: referer_url.href,
target_map, target_map,
page_cookie,
}); });
heartbeat = setInterval(() => { heartbeat = setInterval(() => {
try { try {
c.enqueue(": keep-alive\n\n"); c.enqueue(": keep-alive\n\n");
} catch { } catch {
clearInterval(heartbeat); clearInterval(heartbeat);
removeController(controller);
} }
}, 5000); }, 5000);
}, },
cancel() { cancel() {
clearInterval(heartbeat); clearInterval(heartbeat);
const targetControllerIndex = global.HMR_CONTROLLERS.findIndex( removeController(controller);
(c) => c.controller == controller,
);
if (
typeof targetControllerIndex == "number" &&
targetControllerIndex >= 0
) {
global.HMR_CONTROLLERS.splice(targetControllerIndex, 1);
}
}, },
}); });
+4 -1
View File
@@ -2,6 +2,7 @@ import grabDirNames from "../../utils/grab-dir-names";
import path from "path"; import path from "path";
import isDevelopment from "../../utils/is-development"; import isDevelopment from "../../utils/is-development";
import { existsSync } from "fs"; import { existsSync } from "fs";
import isSafePath from "../../utils/is-safe-path";
const { PUBLIC_DIR } = grabDirNames(); const { PUBLIC_DIR } = grabDirNames();
@@ -19,7 +20,7 @@ export default async function ({ req }: Params): Promise<Response> {
url.pathname.replace(/^\/public/, ""), url.pathname.replace(/^\/public/, ""),
); );
if (!file_path.startsWith(PUBLIC_DIR + path.sep)) { if (!isSafePath({ filePath: file_path, allowedDir: PUBLIC_DIR })) {
return new Response("Forbidden", { status: 403 }); return new Response("Forbidden", { status: 403 });
} }
@@ -53,6 +54,8 @@ export function readFileResponse({ file_path, cache }: FileResponse) {
headers.set("Cache-Control", "public, max-age=31536000, immutable"); headers.set("Cache-Control", "public, max-age=31536000, immutable");
} else if (cache?.duration) { } else if (cache?.duration) {
headers.set("Cache-Control", `public, max-age=${cache.duration}`); headers.set("Cache-Control", `public, max-age=${cache.duration}`);
} else if (!isDevelopment()) {
headers.set("Cache-Control", "public, max-age=3600");
} }
return new Response(file, { return new Response(file, {
+25 -7
View File
@@ -52,10 +52,10 @@ export default async function ({ req }: Params): Promise<Response> {
let module: any; let module: any;
const now = Date.now(); const now = Date.now();
if (is_dev && global.SSR_BUNDLER_CTX_MAP?.[match.filePath]?.path) { if (is_dev && global.BUNEXT_SSR_BUNDLER_CTX_MAP?.[match.filePath]?.path) {
const target_import = path.join( const target_import = path.join(
ROOT_DIR, ROOT_DIR,
global.SSR_BUNDLER_CTX_MAP[match.filePath].path, global.BUNEXT_SSR_BUNDLER_CTX_MAP[match.filePath].path,
); );
module = await import(`${target_import}?t=${now}`); module = await import(`${target_import}?t=${now}`);
@@ -68,16 +68,16 @@ export default async function ({ req }: Params): Promise<Response> {
const config = module.config as BunextServerRouteConfig | undefined; const config = module.config as BunextServerRouteConfig | undefined;
const maxBodyBytes = config?.max_request_body_mb
? config.max_request_body_mb * MBInBytes
: ServerDefaultRequestBodyLimitBytes;
const contentLength = req.headers.get("content-length"); const contentLength = req.headers.get("content-length");
if (contentLength) { if (contentLength) {
const size = parseInt(contentLength, 10); const size = parseInt(contentLength, 10);
if ( if (size > maxBodyBytes) {
(config?.max_request_body_mb &&
size > config.max_request_body_mb * MBInBytes) ||
size > ServerDefaultRequestBodyLimitBytes
) {
return Response.json( return Response.json(
{ {
success: false, success: false,
@@ -91,6 +91,24 @@ export default async function ({ req }: Params): Promise<Response> {
}, },
); );
} }
} else if (req.method !== "GET" && req.method !== "HEAD") {
const body = await req.arrayBuffer();
if (body.byteLength > maxBodyBytes) {
return Response.json(
{
success: false,
msg: "Request Body Too Large!",
},
{
status: 413,
headers: {
"Content-Type": "application/json",
},
},
);
}
routeParams.body = JSON.parse(new TextDecoder().decode(body) || "{}");
} }
const target_module = (module["default"] || const target_module = (module["default"] ||
+27 -25
View File
@@ -7,61 +7,64 @@ type Params = {
}; };
export default async function serverPostBuildFn(params?: Params) { export default async function serverPostBuildFn(params?: Params) {
if (!global.HMR_CONTROLLERS?.[0] || !global.BUNDLER_CTX_MAP) { if (!global.BUNEXT_HMR_CONTROLLERS?.[0] || !global.BUNEXT_BUNDLER_CTX_MAP) {
return; return;
} }
const reload_payload = { reload: true }; const reload_payload = { reload: true };
const reload_enqueue = `event: update\ndata: ${JSON.stringify(reload_payload)}\n\n`; const reload_enqueue = `event: update\ndata: ${JSON.stringify(reload_payload)}\n\n`;
for (let i = global.HMR_CONTROLLERS.length - 1; i >= 0; i--) { for (let i = global.BUNEXT_HMR_CONTROLLERS.length - 1; i >= 0; i--) {
const controller = global.HMR_CONTROLLERS[i]; const controller = global.BUNEXT_HMR_CONTROLLERS[i];
if (!controller) { if (!controller) {
continue; continue;
} }
if (!controller.target_map?.local_path) { if (!controller.target_map?.local_path) {
// if (global.IS_404_PAGE) {
// controller.controller.enqueue(reload_enqueue);
// }
// if (!global.HMR_CONTROLLERS[i].page_reloaded) {
// controller.controller.enqueue(reload_enqueue);
// global.HMR_CONTROLLERS[i].page_reloaded = true;
// }
continue; continue;
} }
if (params?.reload_all_controllers) { if (params?.reload_all_controllers) {
try {
controller.controller.enqueue(reload_enqueue); controller.controller.enqueue(reload_enqueue);
} catch {
global.BUNEXT_HMR_CONTROLLERS.splice(i, 1);
}
continue; continue;
} }
const target_artifact = const target_artifact =
global.BUNDLER_CTX_MAP[controller.target_map.local_path]; global.BUNEXT_BUNDLER_CTX_MAP[controller.target_map.local_path];
if (!target_artifact.local_path) { if (!target_artifact?.local_path) {
try {
controller.controller.enqueue(reload_enqueue); controller.controller.enqueue(reload_enqueue);
} catch {
global.BUNEXT_HMR_CONTROLLERS.splice(i, 1);
}
continue; continue;
} }
const mock_req = target_artifact.req const mock_req = target_artifact.req_url
? target_artifact.req.clone() ? new Request(target_artifact.req_url, {})
: new Request(controller.page_url); : new Request(controller.page_url);
const page_component = global.IS_SERVER_COMPONENT if (controller.page_cookie) {
? await grabPageComponent({ mock_req.headers.set("cookie", controller.page_cookie);
}
const page_component = await grabPageComponent({
req: mock_req, req: mock_req,
return_server_res_only: true, return_server_res_only: true,
is_hydration: true, is_hydration: true,
}) });
: {};
if (page_component instanceof Response) { if (page_component instanceof Response) {
continue; continue;
} }
const { serverRes } = page_component; const { serverRes } = page_component || {};
const final_artifact: Omit<GlobalHMRControllerObject, "controller"> = { const final_artifact: Omit<GlobalHMRControllerObject, "controller"> = {
..._.omit(controller, ["controller"]), ..._.omit(controller, ["controller"]),
@@ -72,14 +75,13 @@ export default async function serverPostBuildFn(params?: Params) {
delete final_artifact.target_map; delete final_artifact.target_map;
} }
if (serverRes) { // Always replace so prior error props cannot linger
final_artifact.page_props = serverRes; final_artifact.page_props = serverRes || {};
}
try { try {
let final_data: { [k: string]: any } = {}; let final_data: { [k: string]: any } = {};
if (global.ROOT_FILE_UPDATED) { if (global.BUNEXT_ROOT_FILE_UPDATED) {
final_data = reload_payload; final_data = reload_payload;
} else { } else {
final_data = final_artifact; final_data = final_artifact;
@@ -89,9 +91,9 @@ export default async function serverPostBuildFn(params?: Params) {
`event: update\ndata: ${JSON.stringify(final_data)}\n\n`, `event: update\ndata: ${JSON.stringify(final_data)}\n\n`,
); );
global.ROOT_FILE_UPDATED = false; global.BUNEXT_ROOT_FILE_UPDATED = false;
} catch { } catch {
global.HMR_CONTROLLERS.splice(i, 1); global.BUNEXT_HMR_CONTROLLERS.splice(i, 1);
} }
} }
} }
+13 -1
View File
@@ -1,15 +1,27 @@
import _ from "lodash"; import _ from "lodash";
import { log } from "../../utils/log"; import { log } from "../../utils/log";
import serverParamsGen from "./server-params-gen"; import serverParamsGen from "./server-params-gen";
import isDevelopment from "../../utils/is-development";
import watcherEsbuildCTX from "./watcher-esbuild-ctx";
export default async function startServer() { export default async function startServer() {
const serverParams = await serverParamsGen(); const serverParams = await serverParamsGen();
const server = Bun.serve(serverParams); const server = Bun.serve(serverParams);
const is_dev = isDevelopment();
global.SERVER = server; global.BUNEXT_SERVER = server;
log.server(`http://${server.hostname}:${server.port}`); log.server(`http://${server.hostname}:${server.port}`);
if (is_dev) {
setInterval(() => {
if (global.BUNEXT_PAGES_SRC_WATCHER) {
global.BUNEXT_PAGES_SRC_WATCHER.close();
watcherEsbuildCTX();
}
}, 5000);
}
return server; return server;
} }
+71 -24
View File
@@ -1,10 +1,11 @@
import { watch, existsSync, statSync } from "fs"; import { watch, existsSync, statSync, glob } from "fs";
import path from "path"; import path from "path";
import grabDirNames from "../../utils/grab-dir-names"; import grabDirNames from "../../utils/grab-dir-names";
import fullRebuild from "./full-rebuild"; import fullRebuild from "./full-rebuild";
import { AppData } from "../../data/app-data"; import { AppData } from "../../data/app-data";
import checkExcludedPatterns from "../../utils/check-excluded-patterns"; import checkExcludedPatterns from "../../utils/check-excluded-patterns";
import pagesSSRBundler from "../bundler/pages-ssr-bundler"; import pagesSSRBundler from "../bundler/pages-ssr-bundler";
import { log } from "../../utils/log";
const { ROOT_DIR, BUNX_BUNDLER_ERROR_EXIT_FILE } = grabDirNames(); const { ROOT_DIR, BUNX_BUNDLER_ERROR_EXIT_FILE } = grabDirNames();
@@ -16,7 +17,39 @@ export default async function watcherEsbuildCTX() {
persistent: true, persistent: true,
}, },
async (event, filename) => { async (event, filename) => {
let owns_recompile = false;
try {
if (!filename) return; if (!filename) return;
const full_file_path = path.join(ROOT_DIR, filename);
if (global.BUNEXT_CONFIG.exclude_watch_patterns) {
for (
let i = 0;
i < global.BUNEXT_CONFIG.exclude_watch_patterns.length;
i++
) {
const watch_pattern =
global.BUNEXT_CONFIG.exclude_watch_patterns[i];
if (watch_pattern instanceof RegExp) {
const is_path_excluded =
watch_pattern.test(filename);
if (is_path_excluded) {
return;
}
} else {
const excluded_path = path.resolve(
ROOT_DIR,
watch_pattern,
);
if (excluded_path == full_file_path) {
return;
}
}
}
}
if (existsSync(BUNX_BUNDLER_ERROR_EXIT_FILE)) { if (existsSync(BUNX_BUNDLER_ERROR_EXIT_FILE)) {
await fullRebuild(); await fullRebuild();
@@ -27,27 +60,29 @@ export default async function watcherEsbuildCTX() {
return; return;
} }
if (global.BUNDLER_CTX_DISPOSED) { if (global.BUNEXT_BUNDLER_CTX_DISPOSED) {
await fullRebuild({ msg: `Restarting Bundler ...` }); await fullRebuild({ msg: `Restarting Bundler ...` });
global.BUNDLER_CTX_DISPOSED = false; return;
} }
if (global.SSR_BUNDLER_CTX_DISPOSED) { if (global.BUNEXT_SSR_BUNDLER_CTX_DISPOSED) {
pagesSSRBundler(); await pagesSSRBundler().catch((error) => {
log.error(`SSR Bundler Error: ${error}`);
});
} }
if (filename.endsWith(AppData["BunextTmpFileExt"])) { if (filename.endsWith(AppData["BunextTmpFileExt"])) {
return; return;
} }
const full_file_path = path.join(ROOT_DIR, filename);
const does_file_exist = existsSync(full_file_path); const does_file_exist = existsSync(full_file_path);
const file_stat = does_file_exist const file_stat = does_file_exist
? statSync(full_file_path) ? statSync(full_file_path)
: undefined; : undefined;
if (full_file_path.match(/\/styles$/)) { if (full_file_path.match(/\/styles$/)) {
global.RECOMPILING = true; owns_recompile = true;
global.BUNEXT_RECOMPILING = true;
await Bun.sleep(1000); await Bun.sleep(1000);
await fullRebuild({ await fullRebuild({
msg: `Detected new \`styles\` directory. Rebuilding ...`, msg: `Detected new \`styles\` directory. Rebuilding ...`,
@@ -68,35 +103,37 @@ export default async function watcherEsbuildCTX() {
} }
const target_files_match = /\.(tsx?|jsx?|css)$/; const target_files_match = /\.(tsx?|jsx?|css)$/;
// const rebuild_skip_paths = /\/pages\/api\//;
if (event !== "rename") { if (event !== "rename") {
if (filename.match(target_files_match)) { if (filename.match(target_files_match)) {
if (global.RECOMPILING) return; if (global.BUNEXT_RECOMPILING) return;
global.RECOMPILING = true; owns_recompile = true;
global.BUNEXT_RECOMPILING = true;
if (filename.match(/.*\.server\.tsx?/)) { if (filename.match(/.*\.server\.tsx?/)) {
global.IS_SERVER_COMPONENT = true; global.BUNEXT_IS_SERVER_COMPONENT = true;
} }
if (global.BUNDLER_CTX) { if (global.BUNEXT_BUNDLER_CTX) {
try { await global.BUNEXT_BUNDLER_CTX.rebuild();
await global.BUNDLER_CTX.rebuild();
} catch (error) {
console.log(`ESBUILD Rebuild Error =>`, error);
}
} }
if (filename.match(/(404|500)\.tsx?/)) { if (filename.match(/(404|500)\.tsx?/)) {
for ( for (
let i = global.HMR_CONTROLLERS.length - 1; let i =
global.BUNEXT_HMR_CONTROLLERS.length - 1;
i >= 0; i >= 0;
i-- i--
) { ) {
const controller = global.HMR_CONTROLLERS[i]; const controller =
global.BUNEXT_HMR_CONTROLLERS[i];
try {
controller?.controller?.enqueue( controller?.controller?.enqueue(
`event: update\ndata: ${JSON.stringify({ reload: true })}\n\n`, `event: update\ndata: ${JSON.stringify({ reload: true })}\n\n`,
); );
} catch {
global.BUNEXT_HMR_CONTROLLERS.splice(i, 1);
}
} }
} }
} }
@@ -111,13 +148,15 @@ export default async function watcherEsbuildCTX() {
return; return;
} }
if (!filename.match(/^src\/pages\/|\.css$/)) return reloadWatcher(); if (!filename.match(/^src\/pages\/|\.css$/))
return reloadWatcher();
if (checkExcludedPatterns({ path: filename })) if (checkExcludedPatterns({ path: filename }))
return reloadWatcher(); return reloadWatcher();
if (filename.match(/ /)) return reloadWatcher(); if (filename.match(/ /)) return reloadWatcher();
if (global.RECOMPILING) return; if (global.BUNEXT_RECOMPILING) return;
owns_recompile = true;
const action = does_file_exist ? "created" : "deleted"; const action = does_file_exist ? "created" : "deleted";
const type = filename.match(/\.css$/) const type = filename.match(/\.css$/)
? "Sylesheet" ? "Sylesheet"
@@ -130,15 +169,23 @@ export default async function watcherEsbuildCTX() {
await fullRebuild({ await fullRebuild({
msg: `${type} ${action}: ${filename}. Rebuilding ...`, 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;
}
}
}, },
); );
global.PAGES_SRC_WATCHER = pages_src_watcher; global.BUNEXT_PAGES_SRC_WATCHER = pages_src_watcher;
} }
function reloadWatcher() { function reloadWatcher() {
if (global.PAGES_SRC_WATCHER) { if (global.BUNEXT_PAGES_SRC_WATCHER) {
global.PAGES_SRC_WATCHER.close(); global.BUNEXT_PAGES_SRC_WATCHER.close();
watcherEsbuildCTX(); watcherEsbuildCTX();
} }
} }
@@ -70,12 +70,18 @@ export default async function genWebHTML({
const final_meta = _.merge(root_meta, page_meta); const final_meta = _.merge(root_meta, page_meta);
// const public_envs = Object.keys(process.env).filter((e) => const public_envs = Object.fromEntries(
// e.startsWith(`NEXT_PUBLIC_`), Object.entries(process.env).filter(([k]) =>
// ); k.startsWith("BUNEXT_PUBLIC_"),
),
);
const client_process = { const client_process = {
env: {}, env: {
NODE_ENV: dev ? "development" : "production",
...public_envs,
...global.BUNEXT_CONFIG.public_envs,
},
}; };
let final_component = ( let final_component = (
@@ -116,7 +122,7 @@ export default async function genWebHTML({
type="importmap" type="importmap"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: JSON.stringify( __html: JSON.stringify(
global.REACT_IMPORTS_MAP, global.BUNEXT_REACT_IMPORTS_MAP,
), ),
}} }}
defer defer
@@ -178,6 +184,8 @@ export default async function genWebHTML({
console.info = () => {}; console.info = () => {};
console.debug = () => {}; console.debug = () => {};
let htmlBody: string;
try {
const stream = await renderToReadableStream(final_component, { const stream = await renderToReadableStream(final_component, {
onError(error: any) { onError(error: any) {
if (error.message.includes('unique "key" prop')) return; if (error.message.includes('unique "key" prop')) return;
@@ -185,9 +193,10 @@ export default async function genWebHTML({
}, },
}); });
const htmlBody = await new Response(stream).text(); htmlBody = await new Response(stream).text();
} finally {
Object.assign(console, originalConsole); Object.assign(console, originalConsole);
}
html += htmlBody; html += htmlBody;
@@ -38,7 +38,7 @@ export default async function grabFilePathModule<T extends any = any>({
outfile: target_cache_file_path, outfile: target_cache_file_path,
}); });
Loader.registry.delete(target_cache_file_path); // Loader.registry.delete(target_cache_file_path);
const module = await import(`${target_cache_file_path}?t=${Date.now()}`); const module = await import(`${target_cache_file_path}?t=${Date.now()}`);
return module as T; return module as T;
@@ -19,10 +19,13 @@ export default async function grabPageBundledReactComponent({
return_tsx_only, return_tsx_only,
}: Params): Promise<GrabPageReactBundledComponentRes | undefined> { }: Params): Promise<GrabPageReactBundledComponentRes | undefined> {
try { try {
if (global.SSR_BUNDLER_CTX_MAP?.[file_path]) { if (global.BUNEXT_SSR_BUNDLER_CTX_MAP?.[file_path]) {
const mod = await import( const abs = path.join(
path.join(ROOT_DIR, global.SSR_BUNDLER_CTX_MAP[file_path].path) ROOT_DIR,
global.BUNEXT_SSR_BUNDLER_CTX_MAP[file_path].path,
); );
// Loader.registry.delete(abs);
const mod = await import(`${abs}?t=${Date.now()}`);
const Main = mod.default as FC; const Main = mod.default as FC;

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