From 05154bb758edd26e96c3437f7b9d0de18b00ad04 Mon Sep 17 00:00:00 2001 From: Benjamin Toby Date: Tue, 17 Mar 2026 17:48:24 +0100 Subject: [PATCH] Bugfix catch all page renaming bundler error --- index.ts | 2 + src/functions/bundler/all-pages-bundler.ts | 82 ++++++++++++++++++---- src/functions/server/rebuild-bundler.tsx | 18 +++++ src/functions/server/watcher.tsx | 65 ++--------------- src/utils/grab-constants.ts | 3 + 5 files changed, 96 insertions(+), 74 deletions(-) create mode 100644 src/functions/server/rebuild-bundler.tsx diff --git a/index.ts b/index.ts index d33da33..30e4ecf 100755 --- a/index.ts +++ b/index.ts @@ -30,12 +30,14 @@ declare global { var BUNDLER_CTX: BuildContext | undefined; var BUNDLER_CTX_MAP: BundlerCTXMap[] | undefined; var IS_FIRST_BUNDLE_READY: boolean; + var BUNDLER_REBUILDS: 0; } global.ORA_SPINNER = ora(); global.ORA_SPINNER.clear(); global.HMR_CONTROLLERS = []; global.IS_FIRST_BUNDLE_READY = false; +global.BUNDLER_REBUILDS = 0; await init(); diff --git a/src/functions/bundler/all-pages-bundler.ts b/src/functions/bundler/all-pages-bundler.ts index 1c0ee12..e9d8768 100644 --- a/src/functions/bundler/all-pages-bundler.ts +++ b/src/functions/bundler/all-pages-bundler.ts @@ -11,6 +11,7 @@ import isDevelopment from "../../utils/is-development"; import type { BundlerCTXMap } from "../../types"; import { execSync } from "child_process"; import grabConstants from "../../utils/grab-constants"; +import rebuildBundler from "../server/rebuild-bundler"; const { HYDRATION_DST_DIR, PAGES_DIR, HYDRATION_DST_DIR_MAP_JSON_FILE } = grabDirNames(); @@ -40,9 +41,15 @@ type Params = { export default async function allPagesBundler(params?: Params) { const pages = grabAllPages({ exclude_api: true }); - const { ClientRootElementIDName, ClientRootComponentWindowName } = - await grabConstants(); + const { + ClientRootElementIDName, + ClientRootComponentWindowName, + MaxBundlerRebuilds, + } = await grabConstants(); + // Use index-based keys so bracket paths (e.g. [[...catch_all]]) never + // appear in any path that esbuild's binary tracks internally. The actual + // file path is only ever used inside our JS plugin callbacks. const virtualEntries: Record = {}; const dev = isDevelopment(); @@ -53,15 +60,16 @@ export default async function allPagesBundler(params?: Params) { const does_root_exist = existsSync(root_component_path); - for (const page of pages) { - const key = page.local_path; + for (let i = 0; i < pages.length; i++) { + const page = pages[i]; + const virtualKey = `page-${i}`; let txt = ``; txt += `import { hydrateRoot } from "react-dom/client";\n`; if (does_root_exist) { txt += `import Root from "${root_component_path}";\n`; } - txt += `import Page from "${page.local_path}";\n\n`; + txt += `import Page from "page-entry:${i}";\n\n`; txt += `const pageProps = window.__PAGE_PROPS__ || {};\n`; if (does_root_exist) { @@ -72,7 +80,7 @@ export default async function allPagesBundler(params?: Params) { txt += `const root = hydrateRoot(document.getElementById("${ClientRootElementIDName}"), component);\n\n`; txt += `window.${ClientRootComponentWindowName} = root;\n`; - virtualEntries[key] = txt; + virtualEntries[virtualKey] = txt; } const virtualPlugin: esbuild.Plugin = { @@ -83,11 +91,33 @@ export default async function allPagesBundler(params?: Params) { namespace: "virtual", })); + build.onResolve({ filter: /^page-entry:/ }, (args) => ({ + path: args.path.replace("page-entry:", ""), + namespace: "page-entry", + })); + build.onLoad({ filter: /.*/, namespace: "virtual" }, (args) => ({ contents: virtualEntries[args.path], loader: "tsx", resolveDir: process.cwd(), })); + + build.onLoad( + { filter: /.*/, namespace: "page-entry" }, + async (args) => { + const page = pages[parseInt(args.path)]; + try { + return { + contents: await readFile(page.local_path, "utf-8"), + loader: "tsx" as const, + resolveDir: path.dirname(page.local_path), + watchFiles: [page.local_path], + }; + } catch (e: any) { + return { errors: [{ text: e.message }] }; + } + }, + ); }, }; @@ -98,19 +128,38 @@ export default async function allPagesBundler(params?: Params) { console.time("build"); }); - build.onEnd((result) => { - if (result.errors.length > 0) return; + build.onEnd(async (result) => { + if (result.errors.length > 0) { + const messages = await esbuild.formatMessages( + result.errors, + { kind: "error", color: true }, + ); + for (const msg of messages) { + process.stderr.write(msg); + } + + global.BUNDLER_REBUILDS++; + + if (global.BUNDLER_REBUILDS > MaxBundlerRebuilds) { + console.error(`Max Rebuilds all failed.`); + process.exit(1); + } + + await rebuildBundler(); + console.timeEnd("build"); + return; + } const artifacts: (BundlerCTXMap | undefined)[] = Object.entries( result.metafile!.outputs, ) .filter(([, meta]) => meta.entryPoint) .map(([outputPath, meta]) => { - const target_page = pages.find((p) => { - return ( - meta.entryPoint === `virtual:${p.local_path}` - ); - }); + const indexMatch = + meta.entryPoint?.match(/^virtual:page-(\d+)$/); + const target_page = indexMatch + ? pages[parseInt(indexMatch[1])] + : undefined; if (!target_page || !meta.entryPoint) { return undefined; @@ -147,6 +196,7 @@ export default async function allPagesBundler(params?: Params) { // ); global.BUNDLER_CTX_MAP = final_artifacts; + global.BUNDLER_REBUILDS = 0; params?.post_build_fn?.({ artifacts: final_artifacts }); } @@ -171,7 +221,7 @@ export default async function allPagesBundler(params?: Params) { execSync(`rm -rf ${HYDRATION_DST_DIR}`); const ctx = await esbuild.context({ - entryPoints: Object.keys(virtualEntries).map((k) => `virtual:${k}`), + entryPoints: Object.keys(virtualEntries).map((k) => `virtual:${k}`), // ["virtual:page-0", ...] outdir: HYDRATION_DST_DIR, bundle: true, minify: !dev, @@ -190,7 +240,9 @@ export default async function allPagesBundler(params?: Params) { jsx: "automatic", }); - await ctx.rebuild(); + await ctx.rebuild().catch((error: any) => { + console.error(`Build failed:`, error.message); + }); if (params?.watch) { global.BUNDLER_CTX = ctx; diff --git a/src/functions/server/rebuild-bundler.tsx b/src/functions/server/rebuild-bundler.tsx new file mode 100644 index 0000000..742531b --- /dev/null +++ b/src/functions/server/rebuild-bundler.tsx @@ -0,0 +1,18 @@ +import allPagesBundler from "../bundler/all-pages-bundler"; +import serverPostBuildFn from "./server-post-build-fn"; + +export default async function rebuildBundler() { + try { + global.ROUTER.reload(); + + await global.BUNDLER_CTX?.dispose(); + global.BUNDLER_CTX = undefined; + + await allPagesBundler({ + watch: true, + post_build_fn: serverPostBuildFn, + }); + } catch (error: any) { + console.error(error); + } +} diff --git a/src/functions/server/watcher.tsx b/src/functions/server/watcher.tsx index 648ec9c..80d24b4 100644 --- a/src/functions/server/watcher.tsx +++ b/src/functions/server/watcher.tsx @@ -1,18 +1,15 @@ import { watch, existsSync } from "fs"; import path from "path"; import grabDirNames from "../../utils/grab-dir-names"; -import serverParamsGen from "./server-params-gen"; -import allPagesBundler from "../bundler/all-pages-bundler"; -import serverPostBuildFn from "./server-post-build-fn"; -import refreshRouter from "../../utils/refresh-router"; +import rebuildBundler from "./rebuild-bundler"; -const { PAGES_DIR } = grabDirNames(); +const { SRC_DIR } = grabDirNames(); -const PAGE_FILE_RE = /\.(tsx?|jsx?)$/; +const PAGE_FILE_RE = /\.(tsx?|jsx?|css)$/; export default function watcher() { watch( - PAGES_DIR, + SRC_DIR, { recursive: true, persistent: true, @@ -28,7 +25,7 @@ export default function watcher() { if (global.RECOMPILING) return; - const fullPath = path.join(PAGES_DIR, filename); + const fullPath = path.join(SRC_DIR, filename); const action = existsSync(fullPath) ? "created" : "deleted"; clearTimeout(global.WATCHER_TIMEOUT); @@ -38,15 +35,7 @@ export default function watcher() { console.log(`Page ${action}: ${filename}. Rebuilding ...`); - global.ROUTER.reload(); - - await global.BUNDLER_CTX?.dispose(); - global.BUNDLER_CTX = undefined; - - await allPagesBundler({ - watch: true, - post_build_fn: serverPostBuildFn, - }); + await rebuildBundler(); } catch (error: any) { console.error(error); } finally { @@ -55,46 +44,4 @@ export default function watcher() { }, 150); }, ); - - // watch(BUNX_HYDRATION_SRC_DIR, async (event, filename) => { - // if (!filename) return; - - // const targetFile = path.join(BUNX_HYDRATION_SRC_DIR, filename); - - // await Bun.build({ - // entrypoints: [targetFile], - // outdir: HYDRATION_DST_DIR, - // minify: true, - // target: "browser", - // format: "esm", - // }); - - // global.SERVER?.publish("__bun_hmr", "update"); - - // setTimeout(() => { - // global.RECOMPILING = false; - // }, 200); - // }); - - // watch(HYDRATION_DST_DIR, async (event, filename) => { - // const encoder = new TextEncoder(); - // global.HMR_CONTROLLER?.enqueue(encoder.encode(`event: update\ndata: reload\n\n`)); - // global.RECOMPILING = false; - // }); - - // let cmd = `bun build`; - - // cmd += ` ${BUNX_HYDRATION_SRC_DIR}/*.tsx --outdir ${HYDRATION_DST_DIR}`; - // cmd += ` --watch --minify`; - - // execSync(cmd, { stdio: "inherit" }); -} - -async function reloadServer() { - const serverParams = await serverParamsGen(); - - console.log(`Reloading Server ...`); - - global.SERVER?.stop(); - global.SERVER = Bun.serve(serverParams); } diff --git a/src/utils/grab-constants.ts b/src/utils/grab-constants.ts index b020846..9ffb168 100644 --- a/src/utils/grab-constants.ts +++ b/src/utils/grab-constants.ts @@ -11,11 +11,14 @@ export default async function grabConstants() { const ServerDefaultRequestBodyLimitBytes = MB_IN_BYTES * 10; + const MaxBundlerRebuilds = 5; + return { ClientRootElementIDName, ClientWindowPagePropsName, MBInBytes: MB_IN_BYTES, ServerDefaultRequestBodyLimitBytes, ClientRootComponentWindowName, + MaxBundlerRebuilds, } as const; }