From f6c7f6b78ceb344008ceb3e0aa8b8f6b3a77b261 Mon Sep 17 00:00:00 2001 From: Benjamin Toby Date: Tue, 24 Mar 2026 20:42:47 +0100 Subject: [PATCH] Refactor bundler function --- src/functions/bundler/all-pages-bundler.ts | 1 - .../all-pages-esbuild-context-bundler.ts | 104 ++++------------- .../grab-artifacts-from-bundled-result.ts | 11 +- .../plugins/esbuild-ctx-artifact-tracker.ts | 83 +++++++++++++ .../bundler/plugins/virtual-files-plugin.ts | 45 ++++++++ src/functions/server/server-post-build-fn.ts | 2 +- src/functions/server/watcher-esbuild-ctx.ts | 18 +++ .../grab-page-bundled-react-component.tsx | 1 + .../server/web-pages/grab-page-component.tsx | 80 ++----------- .../web-pages/grab-page-error-component.tsx | 58 +++++++--- .../server/web-pages/grab-page-modules.tsx | 109 ++++++++++++++++++ .../grab-web-page-hydration-script.tsx | 2 +- src/types/index.ts | 1 - src/utils/grab-all-pages.ts | 3 +- 14 files changed, 337 insertions(+), 181 deletions(-) create mode 100644 src/functions/bundler/plugins/esbuild-ctx-artifact-tracker.ts create mode 100644 src/functions/bundler/plugins/virtual-files-plugin.ts create mode 100644 src/functions/server/web-pages/grab-page-modules.tsx diff --git a/src/functions/bundler/all-pages-bundler.ts b/src/functions/bundler/all-pages-bundler.ts index 5c6809d..5217d44 100644 --- a/src/functions/bundler/all-pages-bundler.ts +++ b/src/functions/bundler/all-pages-bundler.ts @@ -90,7 +90,6 @@ export default async function allPagesBundler(params?: Params) { if (build_starts == MAX_BUILD_STARTS) { const error_msg = `Build Failed. Please check all your components and imports.`; log.error(error_msg); - process.exit(1); } }); diff --git a/src/functions/bundler/all-pages-esbuild-context-bundler.ts b/src/functions/bundler/all-pages-esbuild-context-bundler.ts index 91bd5e8..f182c96 100644 --- a/src/functions/bundler/all-pages-esbuild-context-bundler.ts +++ b/src/functions/bundler/all-pages-esbuild-context-bundler.ts @@ -2,26 +2,17 @@ import * as esbuild from "esbuild"; import grabAllPages from "../../utils/grab-all-pages"; import grabDirNames from "../../utils/grab-dir-names"; import isDevelopment from "../../utils/is-development"; -import { log } from "../../utils/log"; import tailwindEsbuildPlugin from "../server/web-pages/tailwind-esbuild-plugin"; import grabClientHydrationScript from "./grab-client-hydration-script"; -import grabArtifactsFromBundledResults from "./grab-artifacts-from-bundled-result"; -import { writeFileSync } from "fs"; import type { PageFiles } from "../../types"; import path from "path"; +import virtualFilesPlugin from "./plugins/virtual-files-plugin"; +import esbuildCTXArtifactTracker from "./plugins/esbuild-ctx-artifact-tracker"; -const { - HYDRATION_DST_DIR, - HYDRATION_DST_DIR_MAP_JSON_FILE, - BUNX_HYDRATION_SRC_DIR, -} = grabDirNames(); - -let build_starts = 0; -const MAX_BUILD_STARTS = 10; +const { HYDRATION_DST_DIR, BUNX_HYDRATION_SRC_DIR } = grabDirNames(); type Params = { post_build_fn?: (params: { artifacts: any[] }) => Promise | void; - // watch?: boolean; }; export default async function allPagesESBuildContextBundler(params?: Params) { @@ -31,82 +22,23 @@ export default async function allPagesESBuildContextBundler(params?: Params) { const dev = isDevelopment(); - const entryToPage = new Map(); + const entryToPage = new Map(); for (const page of pages) { - const txt = await grabClientHydrationScript({ + const tsx = await grabClientHydrationScript({ page_local_path: page.local_path, }); - if (!txt) continue; + if (!tsx) continue; const entryFile = path.join( BUNX_HYDRATION_SRC_DIR, `${page.url_path}.tsx`, ); - await Bun.write(entryFile, txt, { createPath: true }); - entryToPage.set(path.resolve(entryFile), page); + + // await Bun.write(entryFile, txt, { createPath: true }); + entryToPage.set(entryFile, { ...page, tsx }); } - let buildStart = 0; - - const artifactTracker: esbuild.Plugin = { - name: "artifact-tracker", - setup(build) { - build.onStart(() => { - build_starts++; - buildStart = performance.now(); - - if (build_starts == MAX_BUILD_STARTS) { - const error_msg = `Build Failed. Please check all your components and imports.`; - log.error(error_msg); - process.exit(1); - } - }); - - build.onEnd((result) => { - if (result.errors.length > 0) { - for (const error of result.errors) { - const loc = error.location; - const location = loc - ? ` ${loc.file}:${loc.line}:${loc.column}` - : ""; - log.error(`[Build]${location} ${error.text}`); - } - return; - } - - const artifacts = grabArtifactsFromBundledResults({ - result, - entryToPage, - }); - - if (artifacts?.[0] && artifacts.length > 0) { - for (let i = 0; i < artifacts.length; i++) { - const artifact = artifacts[i]; - if (artifact?.local_path && global.BUNDLER_CTX_MAP) { - global.BUNDLER_CTX_MAP[artifact.local_path] = - artifact; - } - } - - params?.post_build_fn?.({ artifacts }); - - // writeFileSync( - // HYDRATION_DST_DIR_MAP_JSON_FILE, - // JSON.stringify(artifacts, null, 4), - // ); - } - - const elapsed = (performance.now() - buildStart).toFixed(0); - log.success(`[Built] in ${elapsed}ms`); - - global.RECOMPILING = false; - - build_starts = 0; - }); - }, - }; - const entryPoints = [...entryToPage.keys()]; const ctx = await esbuild.context({ @@ -124,10 +56,20 @@ export default async function allPagesESBuildContextBundler(params?: Params) { }, entryNames: "[dir]/[hash]", metafile: true, - plugins: [tailwindEsbuildPlugin, artifactTracker], + plugins: [ + tailwindEsbuildPlugin, + virtualFilesPlugin({ + entryToPage, + }), + esbuildCTXArtifactTracker({ + entryToPage, + post_build_fn: params?.post_build_fn, + }), + ], jsx: "automatic", splitting: true, - // logLevel: "silent", + logLevel: "silent", + // logLevel: dev ? "error" : "silent", external: [ "react", "react-dom", @@ -138,9 +80,5 @@ export default async function allPagesESBuildContextBundler(params?: Params) { await ctx.rebuild(); - // if (params?.watch) { - // await ctx.watch(); - // } - global.BUNDLER_CTX = ctx; } diff --git a/src/functions/bundler/grab-artifacts-from-bundled-result.ts b/src/functions/bundler/grab-artifacts-from-bundled-result.ts index d076948..00df589 100644 --- a/src/functions/bundler/grab-artifacts-from-bundled-result.ts +++ b/src/functions/bundler/grab-artifacts-from-bundled-result.ts @@ -7,7 +7,12 @@ const { ROOT_DIR } = grabDirNames(); type Params = { result: esbuild.BuildResult; - entryToPage: Map; + entryToPage: Map< + string, + PageFiles & { + tsx: string; + } + >; }; export default function grabArtifactsFromBundledResults({ @@ -29,8 +34,7 @@ export default function grabArtifactsFromBundledResults({ return undefined; } - const { file_name, local_path, url_path, transformed_path } = - target_page; + const { file_name, local_path, url_path } = target_page; return { path: outputPath, @@ -43,7 +47,6 @@ export default function grabArtifactsFromBundledResults({ file_name, local_path, url_path, - transformed_path, }; }); diff --git a/src/functions/bundler/plugins/esbuild-ctx-artifact-tracker.ts b/src/functions/bundler/plugins/esbuild-ctx-artifact-tracker.ts new file mode 100644 index 0000000..3a9ea62 --- /dev/null +++ b/src/functions/bundler/plugins/esbuild-ctx-artifact-tracker.ts @@ -0,0 +1,83 @@ +import type { Plugin } from "esbuild"; +import type { PageFiles } from "../../../types"; +import { log } from "../../../utils/log"; +import grabArtifactsFromBundledResults from "../grab-artifacts-from-bundled-result"; + +let buildStart = 0; +let build_starts = 0; +const MAX_BUILD_STARTS = 10; + +type Params = { + entryToPage: Map< + string, + PageFiles & { + tsx: string; + } + >; + post_build_fn?: (params: { artifacts: any[] }) => Promise | void; +}; + +export default function esbuildCTXArtifactTracker({ + entryToPage, + post_build_fn, +}: Params) { + const artifactTracker: Plugin = { + name: "artifact-tracker", + setup(build) { + build.onStart(() => { + build_starts++; + buildStart = performance.now(); + + if (build_starts == MAX_BUILD_STARTS) { + const error_msg = `Build Failed. Please check all your components and imports.`; + log.error(error_msg); + global.BUNDLER_CTX?.cancel(); + } + }); + + build.onEnd((result) => { + if (result.errors.length > 0) { + // for (const error of result.errors) { + // const loc = error.location; + // const location = loc + // ? ` ${loc.file}:${loc.line}:${loc.column}` + // : ""; + // log.error(`[Build]${location} ${error.text}`); + // } + return; + } + + const artifacts = grabArtifactsFromBundledResults({ + result, + entryToPage, + }); + + if (artifacts?.[0] && artifacts.length > 0) { + for (let i = 0; i < artifacts.length; i++) { + const artifact = artifacts[i]; + if (artifact?.local_path && global.BUNDLER_CTX_MAP) { + global.BUNDLER_CTX_MAP[artifact.local_path] = + artifact; + } + } + + post_build_fn?.({ artifacts }); + + // writeFileSync( + // HYDRATION_DST_DIR_MAP_JSON_FILE, + // JSON.stringify(artifacts, null, 4), + // ); + } + + const elapsed = (performance.now() - buildStart).toFixed(0); + log.success(`[Built] in ${elapsed}ms`); + + global.RECOMPILING = false; + + build_starts = 0; + }); + }, + }; + + return artifactTracker; +} diff --git a/src/functions/bundler/plugins/virtual-files-plugin.ts b/src/functions/bundler/plugins/virtual-files-plugin.ts new file mode 100644 index 0000000..6fea90f --- /dev/null +++ b/src/functions/bundler/plugins/virtual-files-plugin.ts @@ -0,0 +1,45 @@ +import type { Plugin } from "esbuild"; +import path from "path"; +import type { PageFiles } from "../../../types"; + +type Params = { + entryToPage: Map< + string, + PageFiles & { + tsx: string; + } + >; +}; + +export default function virtualFilesPlugin({ entryToPage }: Params) { + const virtualPlugin: Plugin = { + name: "virtual-hydration", + setup(build) { + build.onResolve({ filter: /^hydration-virtual:/ }, (args) => { + const final_path = args.path.replace(/hydration-virtual:/, ""); + return { + path: final_path, + namespace: "hydration-virtual", + }; + }); + + build.onLoad( + { filter: /.*/, namespace: "hydration-virtual" }, + (args) => { + const target = entryToPage.get(args.path); + if (!target?.tsx) return null; + + const contents = target.tsx; + + return { + contents: contents || "", + loader: "tsx", + resolveDir: path.dirname(target.local_path), + }; + }, + ); + }, + }; + + return virtualPlugin; +} diff --git a/src/functions/server/server-post-build-fn.ts b/src/functions/server/server-post-build-fn.ts index ca3fbda..4ee9b74 100644 --- a/src/functions/server/server-post-build-fn.ts +++ b/src/functions/server/server-post-build-fn.ts @@ -14,7 +14,7 @@ export default async function serverPostBuildFn() { for (let i = global.HMR_CONTROLLERS.length - 1; i >= 0; i--) { const controller = global.HMR_CONTROLLERS[i]; - if (!controller.target_map?.local_path) { + if (!controller?.target_map?.local_path) { continue; } diff --git a/src/functions/server/watcher-esbuild-ctx.ts b/src/functions/server/watcher-esbuild-ctx.ts index 63ee32f..8675e38 100644 --- a/src/functions/server/watcher-esbuild-ctx.ts +++ b/src/functions/server/watcher-esbuild-ctx.ts @@ -18,6 +18,10 @@ export default async function watcherEsbuildCTX() { async (event, filename) => { if (!filename) return; + if (filename.match(/^\.\w+/)) { + return; + } + const full_file_path = path.join(ROOT_DIR, filename); if (full_file_path.match(/\/styles$/)) { @@ -47,7 +51,21 @@ export default async function watcherEsbuildCTX() { if (filename.match(target_files_match)) { if (global.RECOMPILING) return; global.RECOMPILING = true; + await global.BUNDLER_CTX?.rebuild(); + + if (filename.match(/(404|500)\.tsx?/)) { + for ( + let i = global.HMR_CONTROLLERS.length - 1; + i >= 0; + i-- + ) { + const controller = global.HMR_CONTROLLERS[i]; + controller?.controller?.enqueue( + `event: update\ndata: ${JSON.stringify({ reload: true })}\n\n`, + ); + } + } } return; } diff --git a/src/functions/server/web-pages/grab-page-bundled-react-component.tsx b/src/functions/server/web-pages/grab-page-bundled-react-component.tsx index 1eae59f..a8d72ce 100644 --- a/src/functions/server/web-pages/grab-page-bundled-react-component.tsx +++ b/src/functions/server/web-pages/grab-page-bundled-react-component.tsx @@ -1,3 +1,4 @@ +import type { JSX } from "react"; import type { GrabPageReactBundledComponentRes } from "../../../types"; import grabPageReactComponentString from "./grab-page-react-component-string"; import grabTsxStringModule from "./grab-tsx-string-module"; diff --git a/src/functions/server/web-pages/grab-page-component.tsx b/src/functions/server/web-pages/grab-page-component.tsx index b496f79..0130cda 100644 --- a/src/functions/server/web-pages/grab-page-component.tsx +++ b/src/functions/server/web-pages/grab-page-component.tsx @@ -14,6 +14,7 @@ import { log } from "../../../utils/log"; import grabRootFilePath from "./grab-root-file-path"; import grabPageServerRes from "./grab-page-server-res"; import grabPageServerPath from "./grab-page-server-path"; +import grabPageModules from "./grab-page-modules"; class NotFoundError extends Error {} @@ -30,7 +31,6 @@ export default async function grabPageComponent({ }: Params): Promise { const url = req?.url ? new URL(req.url) : undefined; const router = global.ROUTER; - const now = Date.now(); let routeParams: BunxRouteParams | undefined = undefined; @@ -78,79 +78,18 @@ export default async function grabPageComponent({ log.info(`bundledMap:`, bundledMap); } - const { root_file_path } = grabRootFilePath(); - const root_module: BunextRootModule | undefined = root_file_path - ? await import(`${root_file_path}?t=${now}`) - : undefined; - const { server_file_path: root_server_file_path } = root_file_path - ? grabPageServerPath({ file_path: root_file_path }) - : {}; - const root_server_module: BunextPageServerModule = root_server_file_path - ? await import(`${root_server_file_path}?t=${now}`) - : undefined; - - const root_server_fn = - root_server_module?.default || root_server_module?.server; - - const rootServerRes: BunextPageModuleServerReturn | undefined = - root_server_fn - ? await grabPageServerRes({ - server_function: root_server_fn, - url, - query: match?.query, - routeParams, - }) - : undefined; - - if (debug) { - log.info(`rootServerRes:`, rootServerRes); - } - - const module: BunextPageModule = await import(`${file_path}?t=${now}`); - const { server_file_path } = grabPageServerPath({ file_path }); - const server_module: BunextPageServerModule = server_file_path - ? await import(`${server_file_path}?t=${now}`) - : undefined; - - if (debug) { - log.info(`module:`, module); - } - - const server_fn = server_module?.default || server_module?.server; - - const serverRes: BunextPageModuleServerReturn | undefined = server_fn - ? await grabPageServerRes({ - server_function: server_fn, - url, - query: match?.query, - routeParams, - }) - : undefined; - - if (debug) { - log.info(`serverRes:`, serverRes); - } - - const mergedServerRes = _.merge(rootServerRes || {}, serverRes || {}); - - const { component } = - (await grabPageBundledReactComponent({ + const { component, module, serverRes, root_module } = + await grabPageModules({ file_path, - root_file_path, - server_res: mergedServerRes, - })) || {}; - - if (!component) { - throw new Error(`Couldn't grab page component`); - } - - if (debug) { - log.info(`component:`, component); - } + debug, + query: match?.query, + routeParams, + url, + }); return { component, - serverRes: mergedServerRes, + serverRes, routeParams, module, bundledMap, @@ -163,6 +102,7 @@ export default async function grabPageComponent({ error, routeParams, is404: error instanceof NotFoundError, + url, }); } } diff --git a/src/functions/server/web-pages/grab-page-error-component.tsx b/src/functions/server/web-pages/grab-page-error-component.tsx index a794e8e..60160e6 100644 --- a/src/functions/server/web-pages/grab-page-error-component.tsx +++ b/src/functions/server/web-pages/grab-page-error-component.tsx @@ -5,17 +5,21 @@ import type { BunxRouteParams, GrabPageComponentRes, } from "../../../types"; +import grabPageModules from "./grab-page-modules"; +import _ from "lodash"; type Params = { error?: any; routeParams?: BunxRouteParams; is404?: boolean; + url?: URL; }; export default async function grabPageErrorComponent({ error, routeParams, is404, + url, }: Params): Promise { const router = global.ROUTER; @@ -27,28 +31,51 @@ export default async function grabPageErrorComponent({ ? BUNX_ROOT_404_PRESET_COMPONENT : BUNX_ROOT_500_PRESET_COMPONENT; + const default_server_res = { + responseOptions: { + status: is404 ? 404 : 500, + }, + }; + try { const match = router.match(errorRoute); - const filePath = match?.filePath || presetComponent; - const bundledMap = match?.filePath - ? global.BUNDLER_CTX_MAP?.[match.filePath] - : undefined; + if (!match?.filePath) { + const default_module: BunextPageModule = await import( + presetComponent + ); + const Component = default_module.default as FC; + const default_jsx = ( + {{error.message}} + ); - const module: BunextPageModule = await import(filePath); - const Component = module.default as FC; - const component = {{error.message}}; + return { + component: default_jsx, + module: default_module, + routeParams, + serverRes: default_server_res, + }; + } + + const file_path = match.filePath; + + const bundledMap = global.BUNDLER_CTX_MAP?.[file_path]; + + const { component, module, serverRes, root_module } = + await grabPageModules({ + file_path: file_path, + query: match?.query, + routeParams, + url, + }); return { component, routeParams, module, bundledMap, - serverRes: { - responseOptions: { - status: is404 ? 404 : 500, - }, - } as any, + serverRes: _.merge(serverRes, default_server_res), + root_module, }; } catch { const DefaultNotFound: FC = () => ( @@ -71,12 +98,7 @@ export default async function grabPageErrorComponent({ component: , routeParams, module: { default: DefaultNotFound }, - bundledMap: undefined, - serverRes: { - responseOptions: { - status: is404 ? 404 : 500, - }, - } as any, + serverRes: default_server_res, }; } } diff --git a/src/functions/server/web-pages/grab-page-modules.tsx b/src/functions/server/web-pages/grab-page-modules.tsx new file mode 100644 index 0000000..e66f2e8 --- /dev/null +++ b/src/functions/server/web-pages/grab-page-modules.tsx @@ -0,0 +1,109 @@ +import type { + BunextPageModule, + BunextPageModuleServerReturn, + BunextPageServerModule, + BunextRootModule, + BunxRouteParams, +} from "../../../types"; +import grabPageBundledReactComponent from "./grab-page-bundled-react-component"; +import _ from "lodash"; +import { log } from "../../../utils/log"; +import grabRootFilePath from "./grab-root-file-path"; +import grabPageServerRes from "./grab-page-server-res"; +import grabPageServerPath from "./grab-page-server-path"; +import type { JSX } from "react"; + +type Params = { + file_path: string; + debug?: boolean; + url?: URL; + query?: any; + routeParams?: BunxRouteParams; +}; + +export default async function grabPageModules({ + file_path, + debug, + url, + query, + routeParams, +}: Params) { + const now = Date.now(); + + const { root_file_path } = grabRootFilePath(); + const root_module: BunextRootModule | undefined = root_file_path + ? await import(`${root_file_path}?t=${now}`) + : undefined; + const { server_file_path: root_server_file_path } = root_file_path + ? grabPageServerPath({ file_path: root_file_path }) + : {}; + const root_server_module: BunextPageServerModule = root_server_file_path + ? await import(`${root_server_file_path}?t=${now}`) + : undefined; + + const root_server_fn = + root_server_module?.default || root_server_module?.server; + + const rootServerRes: BunextPageModuleServerReturn | undefined = + root_server_fn + ? await grabPageServerRes({ + server_function: root_server_fn, + url, + query, + routeParams, + }) + : undefined; + + if (debug) { + log.info(`rootServerRes:`, rootServerRes); + } + + const module: BunextPageModule = await import(`${file_path}?t=${now}`); + const { server_file_path } = grabPageServerPath({ file_path }); + const server_module: BunextPageServerModule = server_file_path + ? await import(`${server_file_path}?t=${now}`) + : undefined; + + if (debug) { + log.info(`module:`, module); + } + + const server_fn = server_module?.default || server_module?.server; + + const serverRes: BunextPageModuleServerReturn | undefined = server_fn + ? await grabPageServerRes({ + server_function: server_fn, + url, + query, + routeParams, + }) + : undefined; + + if (debug) { + log.info(`serverRes:`, serverRes); + } + + const mergedServerRes = _.merge(rootServerRes || {}, serverRes || {}); + + const { component } = + (await grabPageBundledReactComponent({ + file_path, + root_file_path, + server_res: mergedServerRes, + })) || {}; + + if (!component) { + throw new Error(`Couldn't grab page component`); + } + + if (debug) { + log.info(`component:`, component); + } + + return { + component, + serverRes: mergedServerRes, + module, + root_module, + }; +} diff --git a/src/functions/server/web-pages/grab-web-page-hydration-script.tsx b/src/functions/server/web-pages/grab-web-page-hydration-script.tsx index 72ae498..dc7daf0 100644 --- a/src/functions/server/web-pages/grab-web-page-hydration-script.tsx +++ b/src/functions/server/web-pages/grab-web-page-hydration-script.tsx @@ -41,7 +41,7 @@ export default async function (params?: Params) { script += ` try {\n`; script += ` document.getElementById("__bunext_error_overlay")?.remove();\n`; script += ` const data = JSON.parse(event.data);\n`; - // script += ` console.log("data", data);\n`; + script += ` console.log("data", data);\n`; script += ` if (data.reload) {\n`; script += ` console.log(\`Root Changes Detected. Reloading Page ...\`);\n`; diff --git a/src/types/index.ts b/src/types/index.ts index f39f102..ebd1f7d 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -283,7 +283,6 @@ export type GrabPageReactBundledComponentRes = { export type PageFiles = { local_path: string; - transformed_path: string; url_path: string; file_name: string; }; diff --git a/src/utils/grab-all-pages.ts b/src/utils/grab-all-pages.ts index 574b293..781df55 100644 --- a/src/utils/grab-all-pages.ts +++ b/src/utils/grab-all-pages.ts @@ -90,11 +90,10 @@ function grabPageFileObject({ let file_name = url_path.split("/").pop(); if (!file_name) return; - const transformed_path = pagePathTransform({ page_path: file_path }); + // const transformed_path = pagePathTransform({ page_path: file_path }); return { local_path: file_path, - transformed_path, url_path, file_name, };