Refactor start and dev commands. Start in child processes. Handle errors better

This commit is contained in:
2026-04-17 08:47:36 +01:00
parent a5f25d522e
commit 9a427412f3
38 changed files with 706 additions and 224 deletions
+18
View File
@@ -0,0 +1,18 @@
import startServer from "../../functions/server/start-server";
import { log } from "../../utils/log";
import bunextInit from "../../functions/bunext-init";
import grabDirNames from "../../utils/grab-dir-names";
import { rmSync } from "fs";
const { HYDRATION_DST_DIR, BUNX_CWD_PAGES_REWRITE_DIR } = grabDirNames();
log.info("Running development server ...");
try {
rmSync(HYDRATION_DST_DIR, { recursive: true });
rmSync(BUNX_CWD_PAGES_REWRITE_DIR, { recursive: true });
} catch (error) {}
await bunextInit();
await startServer();
+27 -22
View File
@@ -1,31 +1,36 @@
import { Command } from "commander";
import startServer from "../../functions/server/start-server";
import { log } from "../../utils/log";
import bunextInit from "../../functions/bunext-init";
import path from "path";
import type { BunSpawnOptions } from "../../types";
import grabDirNames from "../../utils/grab-dir-names";
import { rmSync } from "fs";
const {
HYDRATION_DST_DIR,
BUNX_CWD_PAGES_REWRITE_DIR,
BUNX_CWD_MODULE_CACHE_DIR,
} = grabDirNames();
import writeErrorFile from "../../functions/write-error-file";
export default function () {
return new Command("dev")
.description("Run development server")
.action(async () => {
process.env.NODE_ENV = "development";
log.info("Running development server ...");
try {
rmSync(HYDRATION_DST_DIR, { recursive: true });
rmSync(BUNX_CWD_PAGES_REWRITE_DIR, { recursive: true });
} catch (error) {}
await bunextInit();
await startServer();
await dev();
});
}
async function dev() {
const dev_spawn_file = path.resolve(__dirname, "dev-spawn.ts");
const spawn_options: BunSpawnOptions = {
cmd: ["bun", dev_spawn_file],
stdio: ["inherit", "inherit", "inherit"],
onExit(subprocess, exitCode, signalCode, error) {
writeErrorFile({ exitCode, error });
},
env: {
...process.env,
NODE_ENV: "development",
},
};
let dev_process = Bun.spawn(spawn_options);
const exited = await dev_process.exited;
if (exited) {
return await dev();
}
}
+28 -9
View File
@@ -1,17 +1,36 @@
import { Command } from "commander";
import startServer from "../../functions/server/start-server";
import { log } from "../../utils/log";
import bunextInit from "../../functions/bunext-init";
import path from "path";
import type { BunSpawnOptions } from "../../types";
import writeErrorFile from "../../functions/write-error-file";
export default function () {
return new Command("start")
.description("Start production server")
.action(async () => {
process.env.NODE_ENV = "production";
log.info("Starting production server ...");
await bunextInit();
await startServer();
await start();
});
}
async function start() {
const dev_spawn_file = path.resolve(__dirname, "prod-spawn.ts");
const spawn_options: BunSpawnOptions = {
cmd: ["bun", dev_spawn_file],
stdio: ["inherit", "inherit", "inherit"],
onExit(subprocess, exitCode, signalCode, error) {
writeErrorFile({ exitCode, error });
},
env: {
...process.env,
NODE_ENV: "production",
},
};
let dev_process = Bun.spawn(spawn_options);
const exited = await dev_process.exited;
if (exited) {
return await start();
}
}
+9
View File
@@ -0,0 +1,9 @@
import bunextInit from "../../functions/bunext-init";
import startServer from "../../functions/server/start-server";
import { log } from "../../utils/log";
log.info("Starting production server ...");
await bunextInit();
await startServer();
+1
View File
@@ -6,4 +6,5 @@ export const AppData = {
BunextClientHydrationScriptID: "bunext-client-hydration-script",
BunextTmpFileExt: ".bunext_tmp.tsx",
BunextHMRRetryRoute: "/.bunext/hmr-retry",
DefaultMaxLogs: 50,
} as const;
@@ -8,8 +8,13 @@ import type { BundlerCTXMap, PageFiles } from "../../types";
import path from "path";
import virtualFilesPlugin from "./plugins/virtual-files-plugin";
import esbuildCTXArtifactTracker from "./plugins/esbuild-ctx-artifact-tracker";
import { existsSync } from "fs";
const { HYDRATION_DST_DIR, BUNX_HYDRATION_SRC_DIR } = grabDirNames();
const {
HYDRATION_DST_DIR,
BUNX_HYDRATION_SRC_DIR,
BUNX_BUNDLER_ERROR_EXIT_FILE,
} = grabDirNames();
type Params = {
post_build_fn?: (params: {
@@ -19,6 +24,10 @@ type Params = {
export default async function allPagesESBuildContextBundler(params?: Params) {
try {
const did_process_exit_because_of_bundler_error = existsSync(
BUNX_BUNDLER_ERROR_EXIT_FILE,
);
const pages = grabAllPages({ exclude_api: true });
global.PAGE_FILES = pages;
@@ -83,6 +92,9 @@ export default async function allPagesESBuildContextBundler(params?: Params) {
"react/jsx-runtime",
"react/jsx-dev-runtime",
],
logLevel: did_process_exit_because_of_bundler_error
? "silent"
: undefined,
});
await global.BUNDLER_CTX.rebuild();
@@ -5,10 +5,17 @@ import grabArtifactsFromBundledResults from "../grab-artifacts-from-bundled-resu
import buildOnstartErrorHandler from "../build-on-start-error-handler";
import _ from "lodash";
import pagesSSRBundler from "../pages-ssr-bundler";
import grabDirNames from "../../../utils/grab-dir-names";
import { cpSync, existsSync, mkdirSync, rmSync } from "fs";
import fullRebuild from "../../server/full-rebuild";
import path from "path";
import cleanupLogsDirs from "../../cleanup-logs-dir";
const { BUNX_BUNDLER_ERROR_EXIT_FILE, BUNX_ERROR_LOGS_DIR } = grabDirNames();
let build_start = 0;
let build_starts = 0;
const MAX_BUILD_STARTS = 5;
const MAX_BUILD_STARTS = 2;
type Params = {
entryToPage: Map<
@@ -31,8 +38,16 @@ export default function esbuildCTXArtifactTracker({
build_starts++;
build_start = performance.now();
if (build_starts == MAX_BUILD_STARTS) {
const does_error_file_exist = existsSync(
BUNX_BUNDLER_ERROR_EXIT_FILE,
);
if (
build_starts == MAX_BUILD_STARTS &&
!does_error_file_exist
) {
await buildOnstartErrorHandler();
process.exit(1);
}
});
@@ -69,7 +84,22 @@ export default function esbuildCTXArtifactTracker({
build_starts = 0;
pagesSSRBundler();
const does_error_file_exist = existsSync(
BUNX_BUNDLER_ERROR_EXIT_FILE,
);
if (does_error_file_exist) {
mkdirSync(BUNX_ERROR_LOGS_DIR, { recursive: true });
cpSync(
BUNX_BUNDLER_ERROR_EXIT_FILE,
path.join(BUNX_ERROR_LOGS_DIR, `${Date.now()}.log`),
);
rmSync(BUNX_BUNDLER_ERROR_EXIT_FILE, { force: true });
cleanupLogsDirs();
fullRebuild();
} else {
pagesSSRBundler();
}
// if (global.SSR_BUNDLER_CTX) {
// global.SSR_BUNDLER_CTX.rebuild();
+6 -6
View File
@@ -102,9 +102,9 @@ export default async function bunextInit() {
}
}
process.on("exit", (code) => {
Bun.spawn([process.execPath, ...process.argv.slice(1)], {
stdio: ["inherit", "inherit", "inherit"],
env: process.env,
});
});
// process.on("exit", (code) => {
// Bun.spawn([process.execPath, ...process.argv.slice(1)], {
// stdio: ["inherit", "inherit", "inherit"],
// env: process.env,
// });
// });
+63
View File
@@ -0,0 +1,63 @@
import path from "path";
import { mkdirSync, readdirSync, rmSync, statSync, writeFileSync } from "fs";
import grabDirNames from "../utils/grab-dir-names";
import grabConstants from "../utils/grab-constants";
import { AppData } from "../data/app-data";
const { BUNX_LOGS_DIR } = grabDirNames();
export default function cleanupLogsDirs() {
const logs_dirs = readdirSync(BUNX_LOGS_DIR);
const { config } = grabConstants();
const MAX_LOGS = config.max_logs || AppData["DefaultMaxLogs"];
for (let i = 0; i < logs_dirs.length; i++) {
const dir = logs_dirs[i];
const full_path = path.join(BUNX_LOGS_DIR, dir);
const path_stats = statSync(full_path);
if (!path_stats.isDirectory()) {
continue;
}
const sub_dir_files = readdirSync(full_path).sort((a, b) => {
const timestamp_a = Number(a.split(".")[0]);
const timestamp_b = Number(b.split(".")[0]);
if (timestamp_a > timestamp_b) return 1;
return -1;
});
for (let j = 0; j < sub_dir_files.length; j++) {
const sub_dir_file = sub_dir_files[j];
const sub_dir_file_full_path = path.join(full_path, sub_dir_file);
const sub_dir_file_Stats = statSync(sub_dir_file_full_path);
if (!sub_dir_file_Stats.isFile()) {
rmSync(sub_dir_file_full_path, {
force: true,
recursive: true,
});
continue;
}
if (j > MAX_LOGS - 1) {
rmSync(sub_dir_file_full_path, { force: true });
}
}
}
}
// log.info("Running development server ...");
// try {
// rmSync(HYDRATION_DST_DIR, { recursive: true });
// rmSync(BUNX_CWD_PAGES_REWRITE_DIR, { recursive: true });
// } catch (error) {}
// await bunextInit();
// await startServer();
+53
View File
@@ -0,0 +1,53 @@
import { spawn } from "bun";
// Only the "supervisor" respawns. The child sets this env var so it won't respawn itself.
const IS_CHILD = process.env.__RESPAWN_CHILD === "1";
let shuttingDown = false;
async function cleanup() {
// Put real cleanup here: close DB handles, servers, file descriptors, timers, etc.
// Must be awaitable — do NOT rely on process.on("exit") for this.
}
function respawn(code: number) {
const child = spawn({
cmd: [process.execPath, ...process.argv.slice(1)],
stdio: ["inherit", "inherit", "inherit"],
env: { ...process.env, __RESPAWN_CHILD: "1" },
// Detach so the child survives independently and gets its own process group.
// Without this, killing the parent's group can take the child with it.
});
// Let the child live on its own.
child.unref?.();
}
async function shutdown(code: number) {
if (shuttingDown) return;
shuttingDown = true;
try {
await cleanup();
} catch (e) {
console.error("cleanup failed:", e);
}
// Only the supervisor respawns, and only on abnormal exit.
if (!IS_CHILD && code !== 0) {
respawn(code);
}
process.exit(code);
}
// Catch the things that actually fire *before* exit, where async works.
process.on("SIGINT", () => shutdown(130));
process.on("SIGTERM", () => shutdown(143));
process.on("uncaughtException", (err) => {
console.error(err);
shutdown(1);
});
process.on("unhandledRejection", (err) => {
console.error(err);
shutdown(1);
});
+3
View File
@@ -1,5 +1,6 @@
import { log } from "../../utils/log";
import allPagesESBuildContextBundler from "../bundler/all-pages-esbuild-context-bundler";
import pagesSSRBundler from "../bundler/pages-ssr-bundler";
import serverPostBuildFn from "./server-post-build-fn";
import watcherEsbuildCTX from "./watcher-esbuild-ctx";
@@ -21,6 +22,8 @@ export default async function fullRebuild(params?: { msg?: string }) {
await global.SSR_BUNDLER_CTX?.dispose();
global.SSR_BUNDLER_CTX = undefined;
await pagesSSRBundler();
allPagesESBuildContextBundler({
post_build_fn: () => {
serverPostBuildFn();
+6 -1
View File
@@ -6,7 +6,7 @@ import { AppData } from "../../data/app-data";
import checkExcludedPatterns from "../../utils/check-excluded-patterns";
import pagesSSRBundler from "../bundler/pages-ssr-bundler";
const { ROOT_DIR } = grabDirNames();
const { ROOT_DIR, BUNX_BUNDLER_ERROR_EXIT_FILE } = grabDirNames();
export default async function watcherEsbuildCTX() {
const pages_src_watcher = watch(
@@ -18,6 +18,11 @@ export default async function watcherEsbuildCTX() {
async (event, filename) => {
if (!filename) return;
if (existsSync(BUNX_BUNDLER_ERROR_EXIT_FILE)) {
await fullRebuild();
return;
}
if (filename.match(/^\.\w+/)) {
return;
}
+34
View File
@@ -0,0 +1,34 @@
import path from "path";
import { mkdirSync, writeFileSync } from "fs";
import grabDirNames from "../utils/grab-dir-names";
const { BUNX_BUNDLER_ERROR_EXIT_FILE } = grabDirNames();
export default function writeErrorFile({
exitCode,
error,
}: {
error?: Bun.ErrorLike;
exitCode: number | null;
}) {
let txt = ``;
txt += `Bunext Error\n`;
txt += `============================================\n`;
txt += `ERROR: ${error?.message}\n`;
txt += `EXIT_CODE: ${exitCode || 0}\n`;
txt += `CALL_STACK: ${error?.stack}\n`;
mkdirSync(path.dirname(BUNX_BUNDLER_ERROR_EXIT_FILE), { recursive: true });
writeFileSync(BUNX_BUNDLER_ERROR_EXIT_FILE, txt);
}
// log.info("Running development server ...");
// try {
// rmSync(HYDRATION_DST_DIR, { recursive: true });
// rmSync(BUNX_CWD_PAGES_REWRITE_DIR, { recursive: true });
// } catch (error) {}
// await bunextInit();
// await startServer();
+9
View File
@@ -70,6 +70,7 @@ export type BunextConfig = {
* from the router
*/
pages_exclude_patterns?: RegExp[];
max_logs?: number;
};
export type BunextConfigMiddlewareParams = {
@@ -377,3 +378,11 @@ export type BunextAPIRouteHandler<
| Promise<BunextAPIRouteHandlerObjectReturn<T>>
| Response
| BunextAPIRouteHandlerObjectReturn<T>;
export type BunSpawnOptions = Bun.SpawnOptions.SpawnOptions<
"inherit",
"inherit",
"inherit"
> & {
cmd: string[];
};
+88 -78
View File
@@ -15,11 +15,17 @@ export default function grabDirNames() {
);
const BUNX_CWD_PAGES_REWRITE_DIR = path.resolve(BUNX_CWD_DIR, "pages");
const BUNX_TMP_DIR = path.resolve(BUNX_CWD_DIR, ".tmp");
const BUNX_BUNDLER_ERROR_EXIT_FILE = path.resolve(
BUNX_TMP_DIR,
"BUNDLER_EXIT.txt",
);
const BUNX_HYDRATION_SRC_DIR = path.resolve(
BUNX_CWD_DIR,
"client",
"hydration-src",
);
const BUNX_LOGS_DIR = path.resolve(BUNX_CWD_DIR, "logs");
const BUNX_ERROR_LOGS_DIR = path.resolve(BUNX_LOGS_DIR, "error");
const BUNEXT_PUBLIC_DIR = path.join(BUNX_CWD_DIR, "public");
const HYDRATION_DST_DIR = path.join(BUNEXT_PUBLIC_DIR, "pages");
@@ -46,71 +52,6 @@ export default function grabDirNames() {
`${BUNX_ROOT_404_FILE_NAME}.tsx`,
);
// const NODE_MODULES_DIR = path.resolve(
// existsSync(path.join(BUNX_ROOT_DIR, "source.md"))
// ? BUNX_ROOT_DIR
// : ROOT_DIR,
// "node_modules",
// );
// const REACT_MODULE_DIR = path.join(NODE_MODULES_DIR, "react");
// const REACT_DOM_MODULE_DIR = path.join(NODE_MODULES_DIR, "react-dom");
// const REACT_PRODUCTION_MODULE = path.join(
// REACT_MODULE_DIR,
// "cjs",
// "react.production.js",
// );
// const REACT_DEVELOPMENT_MODULE = path.join(
// REACT_MODULE_DIR,
// "cjs",
// "react.development.js",
// );
// const REACT_JSX_RUNTIME_PRODUCTION_MODULE = path.join(
// REACT_MODULE_DIR,
// "cjs",
// "react-jsx-runtime.production.js",
// );
// const REACT_JSX_RUNTIME_DEVELOPMENT_MODULE = path.join(
// REACT_MODULE_DIR,
// "cjs",
// "react-jsx-runtime.development.js",
// );
// const REACT_JSX_DEVELOPMENT_RUNTIME_PRODUCTION_MODULE = path.join(
// REACT_MODULE_DIR,
// "cjs",
// "react-jsx-dev-runtime.production.js",
// );
// const REACT_JSX_DEVELOPMENT_RUNTIME_DEVELOPMENT_MODULE = path.join(
// REACT_MODULE_DIR,
// "cjs",
// "react-jsx-dev-runtime.development.js",
// );
// const REACT_DOM_PRODUCTION_MODULE = path.join(
// REACT_DOM_MODULE_DIR,
// "cjs",
// "react-dom.production.js",
// );
// const REACT_DOM_DEVELOPMENT_MODULE = path.join(
// REACT_DOM_MODULE_DIR,
// "cjs",
// "react-dom.development.js",
// );
// const REACT_DOM_CLIENT_PRODUCTION_MODULE = path.join(
// REACT_DOM_MODULE_DIR,
// "cjs",
// "react-dom-client.production.js",
// );
// const REACT_DOM_CLIENT_DEVELOPMENT_MODULE = path.join(
// REACT_DOM_MODULE_DIR,
// "cjs",
// "react-dom-client.development.js",
// );
return {
ROOT_DIR,
SRC_DIR,
@@ -136,18 +77,87 @@ export default function grabDirNames() {
HYDRATION_DST_DIR_MAP_JSON_FILE_NAME,
BUNEXT_VENDOR_DIR,
BUNEXT_PUBLIC_DIR,
// NODE_MODULES_DIR,
// REACT_MODULE_DIR,
// REACT_DOM_MODULE_DIR,
// REACT_PRODUCTION_MODULE,
// REACT_DEVELOPMENT_MODULE,
// REACT_JSX_RUNTIME_PRODUCTION_MODULE,
// REACT_JSX_RUNTIME_DEVELOPMENT_MODULE,
// REACT_JSX_DEVELOPMENT_RUNTIME_PRODUCTION_MODULE,
// REACT_JSX_DEVELOPMENT_RUNTIME_DEVELOPMENT_MODULE,
// REACT_DOM_PRODUCTION_MODULE,
// REACT_DOM_DEVELOPMENT_MODULE,
// REACT_DOM_CLIENT_PRODUCTION_MODULE,
// REACT_DOM_CLIENT_DEVELOPMENT_MODULE,
BUNX_BUNDLER_ERROR_EXIT_FILE,
BUNX_ERROR_LOGS_DIR,
BUNX_LOGS_DIR,
};
}
// const NODE_MODULES_DIR = path.resolve(
// existsSync(path.join(BUNX_ROOT_DIR, "source.md"))
// ? BUNX_ROOT_DIR
// : ROOT_DIR,
// "node_modules",
// );
// const REACT_MODULE_DIR = path.join(NODE_MODULES_DIR, "react");
// const REACT_DOM_MODULE_DIR = path.join(NODE_MODULES_DIR, "react-dom");
// const REACT_PRODUCTION_MODULE = path.join(
// REACT_MODULE_DIR,
// "cjs",
// "react.production.js",
// );
// const REACT_DEVELOPMENT_MODULE = path.join(
// REACT_MODULE_DIR,
// "cjs",
// "react.development.js",
// );
// const REACT_JSX_RUNTIME_PRODUCTION_MODULE = path.join(
// REACT_MODULE_DIR,
// "cjs",
// "react-jsx-runtime.production.js",
// );
// const REACT_JSX_RUNTIME_DEVELOPMENT_MODULE = path.join(
// REACT_MODULE_DIR,
// "cjs",
// "react-jsx-runtime.development.js",
// );
// const REACT_JSX_DEVELOPMENT_RUNTIME_PRODUCTION_MODULE = path.join(
// REACT_MODULE_DIR,
// "cjs",
// "react-jsx-dev-runtime.production.js",
// );
// const REACT_JSX_DEVELOPMENT_RUNTIME_DEVELOPMENT_MODULE = path.join(
// REACT_MODULE_DIR,
// "cjs",
// "react-jsx-dev-runtime.development.js",
// );
// const REACT_DOM_PRODUCTION_MODULE = path.join(
// REACT_DOM_MODULE_DIR,
// "cjs",
// "react-dom.production.js",
// );
// const REACT_DOM_DEVELOPMENT_MODULE = path.join(
// REACT_DOM_MODULE_DIR,
// "cjs",
// "react-dom.development.js",
// );
// const REACT_DOM_CLIENT_PRODUCTION_MODULE = path.join(
// REACT_DOM_MODULE_DIR,
// "cjs",
// "react-dom-client.production.js",
// );
// const REACT_DOM_CLIENT_DEVELOPMENT_MODULE = path.join(
// REACT_DOM_MODULE_DIR,
// "cjs",
// "react-dom-client.development.js",
// );
// NODE_MODULES_DIR,
// REACT_MODULE_DIR,
// REACT_DOM_MODULE_DIR,
// REACT_PRODUCTION_MODULE,
// REACT_DEVELOPMENT_MODULE,
// REACT_JSX_RUNTIME_PRODUCTION_MODULE,
// REACT_JSX_RUNTIME_DEVELOPMENT_MODULE,
// REACT_JSX_DEVELOPMENT_RUNTIME_PRODUCTION_MODULE,
// REACT_JSX_DEVELOPMENT_RUNTIME_DEVELOPMENT_MODULE,
// REACT_DOM_PRODUCTION_MODULE,
// REACT_DOM_DEVELOPMENT_MODULE,
// REACT_DOM_CLIENT_PRODUCTION_MODULE,
// REACT_DOM_CLIENT_DEVELOPMENT_MODULE,