From 7fb1784b9589db1032728bfbb9a8ce20e16788e2 Mon Sep 17 00:00:00 2001 From: Benjamin Toby Date: Thu, 9 Apr 2026 23:50:58 +0100 Subject: [PATCH] Bugfix: Refactor SSR bundler. Fix stale cache SSR modules. --- .../grab-artifacts-from-bundled-result.d.ts | 3 +- .../grab-artifacts-from-bundled-result.js | 9 +-- .../bundler/pages-ssr-context-bundler.d.ts | 7 ++ .../bundler/pages-ssr-context-bundler.js | 63 +++++++++++++++ .../plugins/esbuild-ctx-artifact-tracker.js | 13 ++- .../plugins/ssr-ctx-artifact-tracker.d.ts | 12 +++ .../plugins/ssr-ctx-artifact-tracker.js | 43 ++++++++++ .../plugins/ssr-virtual-files-plugin.d.ts | 9 +++ .../plugins/ssr-virtual-files-plugin.js | 28 +++++++ dist/functions/bunext-init.d.ts | 4 + dist/functions/bunext-init.js | 14 ++-- dist/functions/server/rebuild-bundler.js | 2 - dist/functions/server/server-post-build-fn.js | 8 -- dist/functions/server/watcher-esbuild-ctx.js | 6 +- .../grab-page-bundled-react-component.js | 8 ++ .../grab-page-react-component-string.js | 9 +-- package.json | 2 +- .../grab-artifacts-from-bundled-result.ts | 12 +-- .../bundler/pages-ssr-context-bundler.ts | 79 +++++++++++++++++++ .../plugins/esbuild-ctx-artifact-tracker.ts | 15 ++-- .../plugins/ssr-ctx-artifact-tracker.ts | 67 ++++++++++++++++ .../plugins/ssr-virtual-files-plugin.ts | 43 ++++++++++ src/functions/bunext-init.ts | 17 ++-- src/functions/server/rebuild-bundler.tsx | 3 - src/functions/server/server-post-build-fn.ts | 11 --- src/functions/server/watcher-esbuild-ctx.ts | 10 +-- .../grab-page-bundled-react-component.tsx | 14 ++++ .../grab-page-react-component-string.tsx | 8 -- 28 files changed, 434 insertions(+), 85 deletions(-) create mode 100644 dist/functions/bundler/pages-ssr-context-bundler.d.ts create mode 100644 dist/functions/bundler/pages-ssr-context-bundler.js create mode 100644 dist/functions/bundler/plugins/ssr-ctx-artifact-tracker.d.ts create mode 100644 dist/functions/bundler/plugins/ssr-ctx-artifact-tracker.js create mode 100644 dist/functions/bundler/plugins/ssr-virtual-files-plugin.d.ts create mode 100644 dist/functions/bundler/plugins/ssr-virtual-files-plugin.js create mode 100644 src/functions/bundler/pages-ssr-context-bundler.ts create mode 100644 src/functions/bundler/plugins/ssr-ctx-artifact-tracker.ts create mode 100644 src/functions/bundler/plugins/ssr-virtual-files-plugin.ts diff --git a/dist/functions/bundler/grab-artifacts-from-bundled-result.d.ts b/dist/functions/bundler/grab-artifacts-from-bundled-result.d.ts index 243e0d5..698cffb 100644 --- a/dist/functions/bundler/grab-artifacts-from-bundled-result.d.ts +++ b/dist/functions/bundler/grab-artifacts-from-bundled-result.d.ts @@ -5,6 +5,7 @@ type Params = { entryToPage: Map; + virtual_match?: string; }; -export default function grabArtifactsFromBundledResults({ result, entryToPage, }: Params): BundlerCTXMap[] | undefined; +export default function grabArtifactsFromBundledResults({ result, entryToPage, virtual_match, }: Params): BundlerCTXMap[] | undefined; export {}; diff --git a/dist/functions/bundler/grab-artifacts-from-bundled-result.js b/dist/functions/bundler/grab-artifacts-from-bundled-result.js index 278dfa1..563ccfd 100644 --- a/dist/functions/bundler/grab-artifacts-from-bundled-result.js +++ b/dist/functions/bundler/grab-artifacts-from-bundled-result.js @@ -3,19 +3,18 @@ import * as esbuild from "esbuild"; import grabDirNames from "../../utils/grab-dir-names"; import { log } from "../../utils/log"; const { ROOT_DIR } = grabDirNames(); -export default function grabArtifactsFromBundledResults({ result, entryToPage, }) { +export default function grabArtifactsFromBundledResults({ result, entryToPage, virtual_match = "hydration-virtual", }) { if (result.errors.length > 0) return; + const virtual_regex = new RegExp(`^${virtual_match}:`); const artifacts = Object.entries(result.metafile.outputs) .filter(([, meta]) => meta.entryPoint) .map(([outputPath, meta]) => { - const entrypoint = meta.entryPoint?.match(/^hydration-virtual:/) - ? meta.entryPoint?.replace(/^hydration-virtual:/, "") + const entrypoint = meta.entryPoint?.match(virtual_regex) + ? meta.entryPoint?.replace(virtual_regex, "") : meta.entryPoint ? path.join(ROOT_DIR, meta.entryPoint) : ""; - // const entrypoint = path.join(ROOT_DIR, meta.entryPoint || ""); - // console.log("entrypoint", entrypoint); const target_page = entryToPage.get(entrypoint); if (!target_page || !meta.entryPoint) { return undefined; diff --git a/dist/functions/bundler/pages-ssr-context-bundler.d.ts b/dist/functions/bundler/pages-ssr-context-bundler.d.ts new file mode 100644 index 0000000..e462db3 --- /dev/null +++ b/dist/functions/bundler/pages-ssr-context-bundler.d.ts @@ -0,0 +1,7 @@ +type Params = { + post_build_fn?: (params: { + artifacts: any[]; + }) => Promise | void; +}; +export default function pagesSSRContextBundler(params?: Params): Promise; +export {}; diff --git a/dist/functions/bundler/pages-ssr-context-bundler.js b/dist/functions/bundler/pages-ssr-context-bundler.js new file mode 100644 index 0000000..e5f3a1d --- /dev/null +++ b/dist/functions/bundler/pages-ssr-context-bundler.js @@ -0,0 +1,63 @@ +import * as esbuild from "esbuild"; +import grabAllPages from "../../utils/grab-all-pages"; +import grabDirNames from "../../utils/grab-dir-names"; +import isDevelopment from "../../utils/is-development"; +import tailwindEsbuildPlugin from "../server/web-pages/tailwind-esbuild-plugin"; +import grabPageReactComponentString from "../server/web-pages/grab-page-react-component-string"; +import grabRootFilePath from "../server/web-pages/grab-root-file-path"; +import ssrVirtualFilesPlugin from "./plugins/ssr-virtual-files-plugin"; +import ssrCTXArtifactTracker from "./plugins/ssr-ctx-artifact-tracker"; +const { BUNX_CWD_MODULE_CACHE_DIR } = grabDirNames(); +export default async function pagesSSRContextBundler(params) { + const pages = grabAllPages({ exclude_api: true }); + const dev = isDevelopment(); + if (global.SSR_BUNDLER_CTX) { + await global.SSR_BUNDLER_CTX.dispose(); + global.SSR_BUNDLER_CTX = undefined; + } + const entryToPage = new Map(); + const { root_file_path } = grabRootFilePath(); + for (const page of pages) { + const tsx = grabPageReactComponentString({ + file_path: page.local_path, + root_file_path, + }); + if (!tsx) + continue; + entryToPage.set(page.local_path, { ...page, tsx }); + } + const entryPoints = [...entryToPage.keys()].map((e) => `ssr-virtual:${e}`); + global.SSR_BUNDLER_CTX = await esbuild.context({ + entryPoints, + outdir: BUNX_CWD_MODULE_CACHE_DIR, + bundle: true, + minify: !dev, + format: "esm", + target: "es2020", + platform: "node", + define: { + "process.env.NODE_ENV": JSON.stringify(dev ? "development" : "production"), + }, + entryNames: "[dir]/[hash]", + metafile: true, + plugins: [ + tailwindEsbuildPlugin, + ssrVirtualFilesPlugin({ + entryToPage, + }), + ssrCTXArtifactTracker({ + entryToPage, + post_build_fn: params?.post_build_fn, + }), + ], + jsx: "automatic", + external: [ + "react", + "react-dom", + "react/jsx-runtime", + "react/jsx-dev-runtime", + ], + logLevel: "silent", + }); + await global.SSR_BUNDLER_CTX.rebuild(); +} diff --git a/dist/functions/bundler/plugins/esbuild-ctx-artifact-tracker.js b/dist/functions/bundler/plugins/esbuild-ctx-artifact-tracker.js index 408cc4a..c3061be 100644 --- a/dist/functions/bundler/plugins/esbuild-ctx-artifact-tracker.js +++ b/dist/functions/bundler/plugins/esbuild-ctx-artifact-tracker.js @@ -1,6 +1,7 @@ import {} from "esbuild"; import { log } from "../../../utils/log"; import grabArtifactsFromBundledResults from "../grab-artifacts-from-bundled-result"; +import pagesSSRContextBundler from "../pages-ssr-context-bundler"; let buildStart = 0; let build_starts = 0; const MAX_BUILD_STARTS = 2; @@ -17,6 +18,8 @@ export default function esbuildCTXArtifactTracker({ entryToPage, post_build_fn, global.BUNDLER_CTX_DISPOSED = true; global.RECOMPILING = false; global.IS_SERVER_COMPONENT = false; + await global.SSR_BUNDLER_CTX?.dispose(); + global.SSR_BUNDLER_CTX = undefined; await global.BUNDLER_CTX?.dispose(); global.BUNDLER_CTX = undefined; } @@ -60,16 +63,18 @@ export default function esbuildCTXArtifactTracker({ entryToPage, post_build_fn, } } 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; global.IS_SERVER_COMPONENT = false; build_starts = 0; + if (global.SSR_BUNDLER_CTX) { + global.SSR_BUNDLER_CTX.rebuild(); + } + else { + pagesSSRContextBundler(); + } }); }, }; diff --git a/dist/functions/bundler/plugins/ssr-ctx-artifact-tracker.d.ts b/dist/functions/bundler/plugins/ssr-ctx-artifact-tracker.d.ts new file mode 100644 index 0000000..a0ef893 --- /dev/null +++ b/dist/functions/bundler/plugins/ssr-ctx-artifact-tracker.d.ts @@ -0,0 +1,12 @@ +import { type Plugin } from "esbuild"; +import type { PageFiles } from "../../../types"; +type Params = { + entryToPage: Map; + post_build_fn?: (params: { + artifacts: any[]; + }) => Promise | void; +}; +export default function ssrCTXArtifactTracker({ entryToPage, post_build_fn, }: Params): Plugin; +export {}; diff --git a/dist/functions/bundler/plugins/ssr-ctx-artifact-tracker.js b/dist/functions/bundler/plugins/ssr-ctx-artifact-tracker.js new file mode 100644 index 0000000..cdfa6e7 --- /dev/null +++ b/dist/functions/bundler/plugins/ssr-ctx-artifact-tracker.js @@ -0,0 +1,43 @@ +import {} from "esbuild"; +import { log } from "../../../utils/log"; +import grabArtifactsFromBundledResults from "../grab-artifacts-from-bundled-result"; +let buildStart = 0; +let build_starts = 0; +const MAX_BUILD_STARTS = 2; +export default function ssrCTXArtifactTracker({ entryToPage, post_build_fn, }) { + const artifactTracker = { + name: "ssr-artifact-tracker", + setup(build) { + build.onStart(async () => { + build_starts++; + buildStart = performance.now(); + if (build_starts == MAX_BUILD_STARTS) { + // const error_msg = `SSR Build Failed. Please check all your components and imports.`; + // log.error(error_msg); + } + }); + build.onEnd((result) => { + if (result.errors.length > 0) { + return; + } + const artifacts = grabArtifactsFromBundledResults({ + result, + entryToPage, + virtual_match: `ssr-virtual`, + }); + if (artifacts?.[0] && artifacts.length > 0) { + for (let i = 0; i < artifacts.length; i++) { + const artifact = artifacts[i]; + if (artifact?.local_path && + global.SSR_BUNDLER_CTX_MAP) { + global.SSR_BUNDLER_CTX_MAP[artifact.local_path] = + artifact; + } + } + post_build_fn?.({ artifacts }); + } + }); + }, + }; + return artifactTracker; +} diff --git a/dist/functions/bundler/plugins/ssr-virtual-files-plugin.d.ts b/dist/functions/bundler/plugins/ssr-virtual-files-plugin.d.ts new file mode 100644 index 0000000..d04fe0c --- /dev/null +++ b/dist/functions/bundler/plugins/ssr-virtual-files-plugin.d.ts @@ -0,0 +1,9 @@ +import type { Plugin } from "esbuild"; +import type { PageFiles } from "../../../types"; +type Params = { + entryToPage: Map; +}; +export default function ssrVirtualFilesPlugin({ entryToPage }: Params): Plugin; +export {}; diff --git a/dist/functions/bundler/plugins/ssr-virtual-files-plugin.js b/dist/functions/bundler/plugins/ssr-virtual-files-plugin.js new file mode 100644 index 0000000..ad7df5d --- /dev/null +++ b/dist/functions/bundler/plugins/ssr-virtual-files-plugin.js @@ -0,0 +1,28 @@ +import path from "path"; +import { log } from "../../../utils/log"; +export default function ssrVirtualFilesPlugin({ entryToPage }) { + const virtualPlugin = { + name: "ssr-virtual-hydration", + setup(build) { + build.onResolve({ filter: /^ssr-virtual:/ }, (args) => { + const final_path = args.path.replace(/ssr-virtual:/, ""); + return { + path: final_path, + namespace: "ssr-virtual", + }; + }); + build.onLoad({ filter: /.*/, namespace: "ssr-virtual" }, (args) => { + const target = entryToPage.get(args.path); + if (!target?.tsx) + return null; + const contents = target.tsx; + return { + contents: contents || "", + loader: "tsx", + resolveDir: path.dirname(target.local_path), + }; + }); + }, + }; + return virtualPlugin; +} diff --git a/dist/functions/bunext-init.d.ts b/dist/functions/bunext-init.d.ts index 91a3511..92c6d0a 100644 --- a/dist/functions/bunext-init.d.ts +++ b/dist/functions/bunext-init.d.ts @@ -19,6 +19,9 @@ declare global { var BUNDLER_CTX_MAP: { [k: string]: BundlerCTXMap; } | undefined; + var SSR_BUNDLER_CTX_MAP: { + [k: string]: BundlerCTXMap; + } | undefined; var BUNDLER_REBUILDS: 0; var PAGES_SRC_WATCHER: FSWatcher | undefined; var CURRENT_VERSION: string | undefined; @@ -26,6 +29,7 @@ declare global { var ROOT_FILE_UPDATED: boolean; var SKIPPED_BROWSER_MODULES: Set; var BUNDLER_CTX: BuildContext | undefined; + var SSR_BUNDLER_CTX: BuildContext | undefined; var DIR_NAMES: ReturnType; var REACT_IMPORTS_MAP: { imports: Record; diff --git a/dist/functions/bunext-init.js b/dist/functions/bunext-init.js index 282ddef..5e7db8e 100644 --- a/dist/functions/bunext-init.js +++ b/dist/functions/bunext-init.js @@ -8,12 +8,12 @@ import watcherEsbuildCTX from "./server/watcher-esbuild-ctx"; import allPagesESBuildContextBundler from "./bundler/all-pages-esbuild-context-bundler"; import serverPostBuildFn from "./server/server-post-build-fn"; import reactModulesBundler from "./bundler/react-modules-bundler"; -import initPages from "./bundler/init-pages"; const dirNames = grabDirNames(); const { PAGES_DIR } = dirNames; export default async function bunextInit() { global.HMR_CONTROLLERS = []; global.BUNDLER_CTX_MAP = {}; + global.SSR_BUNDLER_CTX_MAP = {}; global.BUNDLER_REBUILDS = 0; global.PAGE_FILES = []; global.SKIPPED_BROWSER_MODULES = new Set(); @@ -34,17 +34,17 @@ export default async function bunextInit() { await allPagesESBuildContextBundler({ post_build_fn: serverPostBuildFn, }); - initPages({ - log_time: true, - }); + // initPages({ + // log_time: true, + // }); watcherEsbuildCTX(); } else { log.build(`Building Modules ...`); await allPagesESBuildContextBundler(); - initPages({ - log_time: true, - }); + // initPages({ + // log_time: true, + // }); cron(); } } diff --git a/dist/functions/server/rebuild-bundler.js b/dist/functions/server/rebuild-bundler.js index e91d069..6869d84 100644 --- a/dist/functions/server/rebuild-bundler.js +++ b/dist/functions/server/rebuild-bundler.js @@ -5,8 +5,6 @@ import cleanupArtifacts from "./cleanup-artifacts"; export default async function rebuildBundler(params) { try { global.ROUTER.reload(); - // await global.BUNDLER_CTX?.dispose(); - // global.BUNDLER_CTX = undefined; const new_artifacts = await allPagesBunBundler({ page_file_paths: params?.target_file_paths, }); diff --git a/dist/functions/server/server-post-build-fn.js b/dist/functions/server/server-post-build-fn.js index 29bfb24..811ef9f 100644 --- a/dist/functions/server/server-post-build-fn.js +++ b/dist/functions/server/server-post-build-fn.js @@ -1,10 +1,6 @@ import _ from "lodash"; import grabPageComponent from "./web-pages/grab-page-component"; -import initPages from "../bundler/init-pages"; export default async function serverPostBuildFn() { - // if (!global.IS_FIRST_BUNDLE_READY) { - // global.IS_FIRST_BUNDLE_READY = true; - // } if (!global.HMR_CONTROLLERS?.[0] || !global.BUNDLER_CTX_MAP) { return; } @@ -45,9 +41,5 @@ export default async function serverPostBuildFn() { catch { global.HMR_CONTROLLERS.splice(i, 1); } - global.REACT_DOM_MODULE_CACHE.delete(target_artifact.local_path); - initPages({ - target_page_file: target_artifact.local_path, - }); } } diff --git a/dist/functions/server/watcher-esbuild-ctx.js b/dist/functions/server/watcher-esbuild-ctx.js index 2fb040f..8d57daf 100644 --- a/dist/functions/server/watcher-esbuild-ctx.js +++ b/dist/functions/server/watcher-esbuild-ctx.js @@ -4,7 +4,6 @@ import grabDirNames from "../../utils/grab-dir-names"; import { log } from "../../utils/log"; import allPagesESBuildContextBundler from "../bundler/all-pages-esbuild-context-bundler"; import serverPostBuildFn from "./server-post-build-fn"; -import initPages from "../bundler/init-pages"; const { ROOT_DIR } = grabDirNames(); export default async function watcherEsbuildCTX() { const pages_src_watcher = watch(ROOT_DIR, { @@ -101,6 +100,9 @@ async function fullRebuild(params) { await global.BUNDLER_CTX?.dispose(); global.BUNDLER_CTX = undefined; global.BUNDLER_CTX_MAP = {}; + await global.SSR_BUNDLER_CTX?.dispose(); + global.SSR_BUNDLER_CTX = undefined; + global.SSR_BUNDLER_CTX_MAP = {}; allPagesESBuildContextBundler({ post_build_fn: serverPostBuildFn, }); @@ -112,12 +114,10 @@ async function fullRebuild(params) { global.PAGES_SRC_WATCHER.close(); watcherEsbuildCTX(); } - initPages(); } function reloadWatcher() { if (global.PAGES_SRC_WATCHER) { global.PAGES_SRC_WATCHER.close(); watcherEsbuildCTX(); } - initPages(); } diff --git a/dist/functions/server/web-pages/grab-page-bundled-react-component.js b/dist/functions/server/web-pages/grab-page-bundled-react-component.js index a1eff06..b86cb1a 100644 --- a/dist/functions/server/web-pages/grab-page-bundled-react-component.js +++ b/dist/functions/server/web-pages/grab-page-bundled-react-component.js @@ -2,8 +2,16 @@ import grabPageReactComponentString from "./grab-page-react-component-string"; import grabTsxStringModule from "./grab-tsx-string-module"; import { log } from "../../../utils/log"; import grabRootFilePath from "./grab-root-file-path"; +import path from "path"; +import grabDirNames from "../../../utils/grab-dir-names"; +const { ROOT_DIR } = grabDirNames(); export default async function grabPageBundledReactComponent({ file_path, return_tsx_only, }) { try { + if (global.SSR_BUNDLER_CTX_MAP?.[file_path]) { + const mod = await import(path.join(ROOT_DIR, global.SSR_BUNDLER_CTX_MAP[file_path].path)); + const Main = mod.default; + return { component: Main }; + } const { root_file_path } = grabRootFilePath(); let tsx = grabPageReactComponentString({ file_path, diff --git a/dist/functions/server/web-pages/grab-page-react-component-string.js b/dist/functions/server/web-pages/grab-page-react-component-string.js index 9f5c09a..28b88e9 100644 --- a/dist/functions/server/web-pages/grab-page-react-component-string.js +++ b/dist/functions/server/web-pages/grab-page-react-component-string.js @@ -1,19 +1,12 @@ -import EJSON from "../../../utils/ejson"; import { log } from "../../../utils/log"; -export default function grabPageReactComponentString({ file_path, root_file_path, -// server_res, - }) { +export default function grabPageReactComponentString({ file_path, root_file_path, }) { try { let tsx = ``; - // const server_res_json = JSON.stringify( - // EJSON.stringify(server_res || {}) ?? "{}", - // ); if (root_file_path) { tsx += `import Root from "${root_file_path}"\n`; } tsx += `import Page from "${file_path}"\n`; tsx += `export default function Main({...props}) {\n\n`; - // tsx += `const props = JSON.parse(${server_res_json})\n\n`; tsx += ` return (\n`; if (root_file_path) { tsx += ` \n`; diff --git a/package.json b/package.json index 87157f7..5147396 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@moduletrace/bunext", - "version": "1.0.62", + "version": "1.0.63", "main": "dist/index.js", "module": "index.ts", "dependencies": { diff --git a/src/functions/bundler/grab-artifacts-from-bundled-result.ts b/src/functions/bundler/grab-artifacts-from-bundled-result.ts index bb4f256..0af068d 100644 --- a/src/functions/bundler/grab-artifacts-from-bundled-result.ts +++ b/src/functions/bundler/grab-artifacts-from-bundled-result.ts @@ -3,7 +3,6 @@ import * as esbuild from "esbuild"; import type { BundlerCTXMap, PageFiles } from "../../types"; import grabDirNames from "../../utils/grab-dir-names"; import { log } from "../../utils/log"; - const { ROOT_DIR } = grabDirNames(); type Params = { @@ -14,28 +13,29 @@ type Params = { tsx: string; } >; + virtual_match?: string; }; export default function grabArtifactsFromBundledResults({ result, entryToPage, + virtual_match = "hydration-virtual", }: Params) { if (result.errors.length > 0) return; + const virtual_regex = new RegExp(`^${virtual_match}:`); + const artifacts: (BundlerCTXMap | undefined)[] = Object.entries( result.metafile!.outputs, ) .filter(([, meta]) => meta.entryPoint) .map(([outputPath, meta]) => { - const entrypoint = meta.entryPoint?.match(/^hydration-virtual:/) - ? meta.entryPoint?.replace(/^hydration-virtual:/, "") + const entrypoint = meta.entryPoint?.match(virtual_regex) + ? meta.entryPoint?.replace(virtual_regex, "") : meta.entryPoint ? path.join(ROOT_DIR, meta.entryPoint) : ""; - // const entrypoint = path.join(ROOT_DIR, meta.entryPoint || ""); - // console.log("entrypoint", entrypoint); - const target_page = entryToPage.get(entrypoint); if (!target_page || !meta.entryPoint) { diff --git a/src/functions/bundler/pages-ssr-context-bundler.ts b/src/functions/bundler/pages-ssr-context-bundler.ts new file mode 100644 index 0000000..eb4155c --- /dev/null +++ b/src/functions/bundler/pages-ssr-context-bundler.ts @@ -0,0 +1,79 @@ +import * as esbuild from "esbuild"; +import grabAllPages from "../../utils/grab-all-pages"; +import grabDirNames from "../../utils/grab-dir-names"; +import isDevelopment from "../../utils/is-development"; +import tailwindEsbuildPlugin from "../server/web-pages/tailwind-esbuild-plugin"; +import type { PageFiles } from "../../types"; +import grabPageReactComponentString from "../server/web-pages/grab-page-react-component-string"; +import grabRootFilePath from "../server/web-pages/grab-root-file-path"; +import ssrVirtualFilesPlugin from "./plugins/ssr-virtual-files-plugin"; +import ssrCTXArtifactTracker from "./plugins/ssr-ctx-artifact-tracker"; + +const { BUNX_CWD_MODULE_CACHE_DIR } = grabDirNames(); + +type Params = { + post_build_fn?: (params: { artifacts: any[] }) => Promise | void; +}; + +export default async function pagesSSRContextBundler(params?: Params) { + const pages = grabAllPages({ exclude_api: true }); + const dev = isDevelopment(); + + if (global.SSR_BUNDLER_CTX) { + await global.SSR_BUNDLER_CTX.dispose(); + global.SSR_BUNDLER_CTX = undefined; + } + + const entryToPage = new Map(); + const { root_file_path } = grabRootFilePath(); + + for (const page of pages) { + const tsx = grabPageReactComponentString({ + file_path: page.local_path, + root_file_path, + }); + + if (!tsx) continue; + + entryToPage.set(page.local_path, { ...page, tsx }); + } + + const entryPoints = [...entryToPage.keys()].map((e) => `ssr-virtual:${e}`); + + global.SSR_BUNDLER_CTX = await esbuild.context({ + entryPoints, + outdir: BUNX_CWD_MODULE_CACHE_DIR, + bundle: true, + minify: !dev, + format: "esm", + target: "es2020", + platform: "node", + define: { + "process.env.NODE_ENV": JSON.stringify( + dev ? "development" : "production", + ), + }, + entryNames: "[dir]/[hash]", + metafile: true, + plugins: [ + tailwindEsbuildPlugin, + ssrVirtualFilesPlugin({ + entryToPage, + }), + ssrCTXArtifactTracker({ + entryToPage, + post_build_fn: params?.post_build_fn, + }), + ], + jsx: "automatic", + external: [ + "react", + "react-dom", + "react/jsx-runtime", + "react/jsx-dev-runtime", + ], + logLevel: "silent", + }); + + await global.SSR_BUNDLER_CTX.rebuild(); +} diff --git a/src/functions/bundler/plugins/esbuild-ctx-artifact-tracker.ts b/src/functions/bundler/plugins/esbuild-ctx-artifact-tracker.ts index 36df0c8..cdc353a 100644 --- a/src/functions/bundler/plugins/esbuild-ctx-artifact-tracker.ts +++ b/src/functions/bundler/plugins/esbuild-ctx-artifact-tracker.ts @@ -2,6 +2,7 @@ import { type Plugin } from "esbuild"; import type { PageFiles } from "../../../types"; import { log } from "../../../utils/log"; import grabArtifactsFromBundledResults from "../grab-artifacts-from-bundled-result"; +import pagesSSRContextBundler from "../pages-ssr-context-bundler"; let buildStart = 0; let build_starts = 0; @@ -37,6 +38,9 @@ export default function esbuildCTXArtifactTracker({ global.RECOMPILING = false; global.IS_SERVER_COMPONENT = false; + await global.SSR_BUNDLER_CTX?.dispose(); + global.SSR_BUNDLER_CTX = undefined; + await global.BUNDLER_CTX?.dispose(); global.BUNDLER_CTX = undefined; } @@ -87,11 +91,6 @@ export default function esbuildCTXArtifactTracker({ } post_build_fn?.({ artifacts }); - - // writeFileSync( - // HYDRATION_DST_DIR_MAP_JSON_FILE, - // JSON.stringify(artifacts, null, 4), - // ); } const elapsed = (performance.now() - buildStart).toFixed(0); @@ -101,6 +100,12 @@ export default function esbuildCTXArtifactTracker({ global.IS_SERVER_COMPONENT = false; build_starts = 0; + + if (global.SSR_BUNDLER_CTX) { + global.SSR_BUNDLER_CTX.rebuild(); + } else { + pagesSSRContextBundler(); + } }); }, }; diff --git a/src/functions/bundler/plugins/ssr-ctx-artifact-tracker.ts b/src/functions/bundler/plugins/ssr-ctx-artifact-tracker.ts new file mode 100644 index 0000000..79678f8 --- /dev/null +++ b/src/functions/bundler/plugins/ssr-ctx-artifact-tracker.ts @@ -0,0 +1,67 @@ +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 = 2; + +type Params = { + entryToPage: Map< + string, + PageFiles & { + tsx: string; + } + >; + post_build_fn?: (params: { artifacts: any[] }) => Promise | void; +}; + +export default function ssrCTXArtifactTracker({ + entryToPage, + post_build_fn, +}: Params) { + const artifactTracker: Plugin = { + name: "ssr-artifact-tracker", + setup(build) { + build.onStart(async () => { + build_starts++; + buildStart = performance.now(); + + if (build_starts == MAX_BUILD_STARTS) { + // const error_msg = `SSR Build Failed. Please check all your components and imports.`; + // log.error(error_msg); + } + }); + + build.onEnd((result) => { + if (result.errors.length > 0) { + return; + } + + const artifacts = grabArtifactsFromBundledResults({ + result, + entryToPage, + virtual_match: `ssr-virtual`, + }); + + if (artifacts?.[0] && artifacts.length > 0) { + for (let i = 0; i < artifacts.length; i++) { + const artifact = artifacts[i]; + if ( + artifact?.local_path && + global.SSR_BUNDLER_CTX_MAP + ) { + global.SSR_BUNDLER_CTX_MAP[artifact.local_path] = + artifact; + } + } + + post_build_fn?.({ artifacts }); + } + }); + }, + }; + + return artifactTracker; +} diff --git a/src/functions/bundler/plugins/ssr-virtual-files-plugin.ts b/src/functions/bundler/plugins/ssr-virtual-files-plugin.ts new file mode 100644 index 0000000..a46e6bb --- /dev/null +++ b/src/functions/bundler/plugins/ssr-virtual-files-plugin.ts @@ -0,0 +1,43 @@ +import type { Plugin } from "esbuild"; +import path from "path"; +import type { PageFiles } from "../../../types"; +import { log } from "../../../utils/log"; + +type Params = { + entryToPage: Map< + string, + PageFiles & { + tsx: string; + } + >; +}; + +export default function ssrVirtualFilesPlugin({ entryToPage }: Params) { + const virtualPlugin: Plugin = { + name: "ssr-virtual-hydration", + setup(build) { + build.onResolve({ filter: /^ssr-virtual:/ }, (args) => { + const final_path = args.path.replace(/ssr-virtual:/, ""); + return { + path: final_path, + namespace: "ssr-virtual", + }; + }); + + build.onLoad({ filter: /.*/, namespace: "ssr-virtual" }, (args) => { + const target = entryToPage.get(args.path); + if (!target?.tsx) return null; + + const contents = target.tsx; + + return { + contents: contents || "", + loader: "tsx", + resolveDir: path.dirname(target.local_path), + }; + }); + }, + }; + + return virtualPlugin; +} diff --git a/src/functions/bunext-init.ts b/src/functions/bunext-init.ts index 612264e..067337e 100644 --- a/src/functions/bunext-init.ts +++ b/src/functions/bunext-init.ts @@ -16,7 +16,7 @@ import watcherEsbuildCTX from "./server/watcher-esbuild-ctx"; import allPagesESBuildContextBundler from "./bundler/all-pages-esbuild-context-bundler"; import serverPostBuildFn from "./server/server-post-build-fn"; import reactModulesBundler from "./bundler/react-modules-bundler"; -import initPages from "./bundler/init-pages"; +// import initPages from "./bundler/init-pages"; /** * # Declare Global Variables @@ -32,6 +32,7 @@ declare global { var HMR_CONTROLLERS: GlobalHMRControllerObject[]; var LAST_BUILD_TIME: number; var BUNDLER_CTX_MAP: { [k: string]: BundlerCTXMap } | undefined; + var SSR_BUNDLER_CTX_MAP: { [k: string]: BundlerCTXMap } | undefined; var BUNDLER_REBUILDS: 0; var PAGES_SRC_WATCHER: FSWatcher | undefined; var CURRENT_VERSION: string | undefined; @@ -39,6 +40,7 @@ declare global { var ROOT_FILE_UPDATED: boolean; var SKIPPED_BROWSER_MODULES: Set; var BUNDLER_CTX: BuildContext | undefined; + var SSR_BUNDLER_CTX: BuildContext | undefined; var DIR_NAMES: ReturnType; var REACT_IMPORTS_MAP: { imports: Record }; var REACT_DOM_SERVER: any; @@ -52,6 +54,7 @@ const { PAGES_DIR } = dirNames; export default async function bunextInit() { global.HMR_CONTROLLERS = []; global.BUNDLER_CTX_MAP = {}; + global.SSR_BUNDLER_CTX_MAP = {}; global.BUNDLER_REBUILDS = 0; global.PAGE_FILES = []; global.SKIPPED_BROWSER_MODULES = new Set(); @@ -78,16 +81,16 @@ export default async function bunextInit() { await allPagesESBuildContextBundler({ post_build_fn: serverPostBuildFn, }); - initPages({ - log_time: true, - }); + // initPages({ + // log_time: true, + // }); watcherEsbuildCTX(); } else { log.build(`Building Modules ...`); await allPagesESBuildContextBundler(); - initPages({ - log_time: true, - }); + // initPages({ + // log_time: true, + // }); cron(); } } diff --git a/src/functions/server/rebuild-bundler.tsx b/src/functions/server/rebuild-bundler.tsx index e1b80b5..71f38f7 100644 --- a/src/functions/server/rebuild-bundler.tsx +++ b/src/functions/server/rebuild-bundler.tsx @@ -11,9 +11,6 @@ export default async function rebuildBundler(params?: Params) { try { global.ROUTER.reload(); - // await global.BUNDLER_CTX?.dispose(); - // global.BUNDLER_CTX = undefined; - const new_artifacts = await allPagesBunBundler({ page_file_paths: params?.target_file_paths, }); diff --git a/src/functions/server/server-post-build-fn.ts b/src/functions/server/server-post-build-fn.ts index 64ca904..d655235 100644 --- a/src/functions/server/server-post-build-fn.ts +++ b/src/functions/server/server-post-build-fn.ts @@ -1,13 +1,8 @@ import _ from "lodash"; import type { GlobalHMRControllerObject } from "../../types"; import grabPageComponent from "./web-pages/grab-page-component"; -import initPages from "../bundler/init-pages"; export default async function serverPostBuildFn() { - // if (!global.IS_FIRST_BUNDLE_READY) { - // global.IS_FIRST_BUNDLE_READY = true; - // } - if (!global.HMR_CONTROLLERS?.[0] || !global.BUNDLER_CTX_MAP) { return; } @@ -61,11 +56,5 @@ export default async function serverPostBuildFn() { } catch { global.HMR_CONTROLLERS.splice(i, 1); } - - global.REACT_DOM_MODULE_CACHE.delete(target_artifact.local_path); - - initPages({ - target_page_file: target_artifact.local_path, - }); } } diff --git a/src/functions/server/watcher-esbuild-ctx.ts b/src/functions/server/watcher-esbuild-ctx.ts index 3f0eb08..a0d01af 100644 --- a/src/functions/server/watcher-esbuild-ctx.ts +++ b/src/functions/server/watcher-esbuild-ctx.ts @@ -4,7 +4,6 @@ import grabDirNames from "../../utils/grab-dir-names"; import { log } from "../../utils/log"; import allPagesESBuildContextBundler from "../bundler/all-pages-esbuild-context-bundler"; import serverPostBuildFn from "./server-post-build-fn"; -import initPages from "../bundler/init-pages"; const { ROOT_DIR } = grabDirNames(); @@ -129,9 +128,12 @@ async function fullRebuild(params?: { msg?: string }) { await global.BUNDLER_CTX?.dispose(); global.BUNDLER_CTX = undefined; - global.BUNDLER_CTX_MAP = {}; + await global.SSR_BUNDLER_CTX?.dispose(); + global.SSR_BUNDLER_CTX = undefined; + global.SSR_BUNDLER_CTX_MAP = {}; + allPagesESBuildContextBundler({ post_build_fn: serverPostBuildFn, }); @@ -143,8 +145,6 @@ async function fullRebuild(params?: { msg?: string }) { global.PAGES_SRC_WATCHER.close(); watcherEsbuildCTX(); } - - initPages(); } function reloadWatcher() { @@ -152,6 +152,4 @@ function reloadWatcher() { global.PAGES_SRC_WATCHER.close(); watcherEsbuildCTX(); } - - initPages(); } 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 2e951ca..023249a 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 @@ -4,6 +4,10 @@ import grabTsxStringModule from "./grab-tsx-string-module"; import { log } from "../../../utils/log"; import type { FC } from "react"; import grabRootFilePath from "./grab-root-file-path"; +import path from "path"; +import grabDirNames from "../../../utils/grab-dir-names"; + +const { ROOT_DIR } = grabDirNames(); type Params = { file_path: string; @@ -15,6 +19,16 @@ export default async function grabPageBundledReactComponent({ return_tsx_only, }: Params): Promise { try { + if (global.SSR_BUNDLER_CTX_MAP?.[file_path]) { + const mod = await import( + path.join(ROOT_DIR, global.SSR_BUNDLER_CTX_MAP[file_path].path) + ); + + const Main = mod.default as FC; + + return { component: Main }; + } + const { root_file_path } = grabRootFilePath(); let tsx = grabPageReactComponentString({ diff --git a/src/functions/server/web-pages/grab-page-react-component-string.tsx b/src/functions/server/web-pages/grab-page-react-component-string.tsx index 0e407bd..88b1ce2 100644 --- a/src/functions/server/web-pages/grab-page-react-component-string.tsx +++ b/src/functions/server/web-pages/grab-page-react-component-string.tsx @@ -1,31 +1,23 @@ -import EJSON from "../../../utils/ejson"; import { log } from "../../../utils/log"; type Params = { file_path: string; root_file_path?: string; - // server_res?: any; }; export default function grabPageReactComponentString({ file_path, root_file_path, - // server_res, }: Params): string | undefined { try { let tsx = ``; - // const server_res_json = JSON.stringify( - // EJSON.stringify(server_res || {}) ?? "{}", - // ); - if (root_file_path) { tsx += `import Root from "${root_file_path}"\n`; } tsx += `import Page from "${file_path}"\n`; tsx += `export default function Main({...props}) {\n\n`; - // tsx += `const props = JSON.parse(${server_res_json})\n\n`; tsx += ` return (\n`; if (root_file_path) { tsx += ` \n`;