Update bundler logic
This commit is contained in:
parent
f6c7f6b78c
commit
336fa812a5
16
README.md
16
README.md
@ -1,6 +1,6 @@
|
||||
# Bunext
|
||||
|
||||
A server-rendering framework for React, built entirely on [Bun](https://bun.sh). Bunext handles file-system routing, SSR, HMR, and client hydration — using `Bun.build` to bundle client assets and `Bun.serve` as the HTTP server.
|
||||
A server-rendering framework for React, built entirely on [Bun](https://bun.sh). Bunext handles file-system routing, SSR, HMR, and client hydration — using ESBuild to bundle client assets and `Bun.serve` as the HTTP server.
|
||||
|
||||
## Philosophy
|
||||
|
||||
@ -8,7 +8,7 @@ Bunext is focused on **server-side rendering and processing**. Every page is ren
|
||||
|
||||
The goal is a framework that is:
|
||||
|
||||
- Fast — Bun's runtime speed and Bun.build's bundling make the full dev loop snappy
|
||||
- Fast — Bun's runtime speed and ESBuild's bundling make the full dev loop snappy
|
||||
- Transparent — the entire request pipeline is readable and debugable
|
||||
- Standard — server functions and API handlers use native Web APIs (`Request`, `Response`, `URL`) with no custom wrappers
|
||||
|
||||
@ -924,7 +924,7 @@ Running `bunext dev`:
|
||||
1. Loads `bunext.config.ts` and sets `development: true`.
|
||||
2. Initializes directories (`.bunext/`, `public/pages/`).
|
||||
3. Creates a `Bun.FileSystemRouter` pointed at `src/pages/`.
|
||||
4. Starts `Bun.build` in **watch mode** — it will automatically rebuild when file content changes.
|
||||
4. Creates an ESBuild context and performs the initial build. File-change rebuilds are triggered manually by the FS watcher.
|
||||
5. Starts a file-system watcher on `src/` — when a file is created or deleted (a "rename" event), it triggers a full bundler rebuild to update the entry points.
|
||||
6. Waits for the first successful bundle.
|
||||
7. Starts `Bun.serve()`.
|
||||
@ -934,7 +934,7 @@ Running `bunext dev`:
|
||||
Running `bunext build`:
|
||||
|
||||
1. Sets `NODE_ENV=production`.
|
||||
2. Runs `Bun.build` once with minification enabled.
|
||||
2. Runs ESBuild once with minification enabled.
|
||||
3. Writes all bundled artifacts to `.bunext/public/pages/` and the artifact map to `.bunext/public/pages/map.json`.
|
||||
4. Exits.
|
||||
|
||||
@ -947,11 +947,13 @@ Running `bunext start`:
|
||||
|
||||
### Bundler
|
||||
|
||||
The bundler uses `Bun.build` with the `bun-plugin-tailwind` plugin. For each page, a client hydration entry point is generated and written as a real temporary file under `.bunext/hydration-src/`. Each entry imports the page component and calls `hydrateRoot()` against the server-rendered DOM node. If `src/pages/__root.tsx` exists, the page is wrapped in the root layout.
|
||||
The bundler uses **ESBuild** with a virtual namespace plugin that generates in-memory hydration entry points for each page — no temporary files are written to disk. Each virtual entry imports the page component and calls `hydrateRoot()` against the server-rendered DOM node. If `src/pages/__root.tsx` exists, the page is wrapped in the root layout. Tailwind CSS is processed via a dedicated ESBuild plugin.
|
||||
|
||||
React is loaded externally — `react`, `react-dom`, `react-dom/client`, and `react/jsx-runtime` are all marked as external in the `Bun.build` config. The correct React version is resolved from the framework's own `node_modules` at startup and injected into every HTML page via a `<script type="importmap">` pointing at `esm.sh`. This guarantees a single shared React instance across all page bundles and HMR updates regardless of project size.
|
||||
In development, an `esbuild.context()` is created once and rebuilt incrementally whenever the FS watcher detects a file change. In production, a single `esbuild.build()` call runs with minification enabled.
|
||||
|
||||
After each build, output metadata from `Bun.build`'s metafile is used to map each output file back to its source page, producing a `BundlerCTXMap[]`. This map is stored in `global.BUNDLER_CTX_MAP` and written to `.bunext/public/pages/map.json`.
|
||||
React is loaded externally — `react`, `react-dom`, `react-dom/client`, and `react/jsx-runtime` are all marked as external in the ESBuild config. The correct React version is resolved from the framework's own `node_modules` at startup and injected into every HTML page via a `<script type="importmap">` pointing at `esm.sh`. This guarantees a single shared React instance across all page bundles and HMR updates regardless of project size.
|
||||
|
||||
After each build, ESBuild's metafile is used to map each output file back to its source page, producing a `BundlerCTXMap[]`. This map is stored in `global.BUNDLER_CTX_MAP` and written to `.bunext/public/pages/map.json`.
|
||||
|
||||
Output files are named `[hash].[ext]` so filenames change when content changes, enabling cache-busting.
|
||||
|
||||
|
||||
@ -0,0 +1,81 @@
|
||||
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 grabClientHydrationScript from "./grab-client-hydration-script";
|
||||
import type { PageFiles } from "../../types";
|
||||
import path from "path";
|
||||
import esbuildCTXArtifactTracker from "./plugins/esbuild-ctx-artifact-tracker";
|
||||
|
||||
const { HYDRATION_DST_DIR, BUNX_HYDRATION_SRC_DIR } = grabDirNames();
|
||||
|
||||
type Params = {
|
||||
post_build_fn?: (params: { artifacts: any[] }) => Promise<void> | void;
|
||||
};
|
||||
|
||||
export default async function allPagesESBuildContextBundlerFiles(
|
||||
params?: Params,
|
||||
) {
|
||||
const pages = grabAllPages({ exclude_api: true });
|
||||
|
||||
global.PAGE_FILES = pages;
|
||||
|
||||
const dev = isDevelopment();
|
||||
|
||||
const entryToPage = new Map<string, PageFiles & { tsx: string }>();
|
||||
|
||||
for (const page of pages) {
|
||||
const tsx = await grabClientHydrationScript({
|
||||
page_local_path: page.local_path,
|
||||
});
|
||||
if (!tsx) continue;
|
||||
|
||||
const entryFile = path.join(
|
||||
BUNX_HYDRATION_SRC_DIR,
|
||||
`${page.url_path}.tsx`,
|
||||
);
|
||||
|
||||
await Bun.write(entryFile, tsx, { createPath: true });
|
||||
entryToPage.set(entryFile, { ...page, tsx });
|
||||
}
|
||||
|
||||
const entryPoints = [...entryToPage.keys()];
|
||||
|
||||
const ctx = await esbuild.context({
|
||||
entryPoints,
|
||||
outdir: HYDRATION_DST_DIR,
|
||||
bundle: true,
|
||||
minify: !dev,
|
||||
format: "esm",
|
||||
target: "es2020",
|
||||
platform: "browser",
|
||||
define: {
|
||||
"process.env.NODE_ENV": JSON.stringify(
|
||||
dev ? "development" : "production",
|
||||
),
|
||||
},
|
||||
entryNames: "[dir]/[hash]",
|
||||
metafile: true,
|
||||
plugins: [
|
||||
tailwindEsbuildPlugin,
|
||||
esbuildCTXArtifactTracker({
|
||||
entryToPage,
|
||||
post_build_fn: params?.post_build_fn,
|
||||
}),
|
||||
],
|
||||
jsx: "automatic",
|
||||
splitting: true,
|
||||
logLevel: "silent",
|
||||
external: [
|
||||
"react",
|
||||
"react-dom",
|
||||
"react-dom/client",
|
||||
"react/jsx-runtime",
|
||||
],
|
||||
});
|
||||
|
||||
await ctx.rebuild();
|
||||
|
||||
global.BUNDLER_CTX = ctx;
|
||||
}
|
||||
@ -16,6 +16,8 @@ type Params = {
|
||||
};
|
||||
|
||||
export default async function allPagesESBuildContextBundler(params?: Params) {
|
||||
// return await allPagesESBuildContextBundlerFiles(params);
|
||||
|
||||
const pages = grabAllPages({ exclude_api: true });
|
||||
|
||||
global.PAGE_FILES = pages;
|
||||
@ -39,9 +41,11 @@ export default async function allPagesESBuildContextBundler(params?: Params) {
|
||||
entryToPage.set(entryFile, { ...page, tsx });
|
||||
}
|
||||
|
||||
const entryPoints = [...entryToPage.keys()];
|
||||
const entryPoints = [...entryToPage.keys()].map(
|
||||
(e) => `hydration-virtual:${e}`,
|
||||
);
|
||||
|
||||
const ctx = await esbuild.context({
|
||||
global.BUNDLER_CTX = await esbuild.context({
|
||||
entryPoints,
|
||||
outdir: HYDRATION_DST_DIR,
|
||||
bundle: true,
|
||||
@ -69,6 +73,7 @@ export default async function allPagesESBuildContextBundler(params?: Params) {
|
||||
jsx: "automatic",
|
||||
splitting: true,
|
||||
logLevel: "silent",
|
||||
// logLevel: "silent",
|
||||
// logLevel: dev ? "error" : "silent",
|
||||
external: [
|
||||
"react",
|
||||
@ -78,7 +83,5 @@ export default async function allPagesESBuildContextBundler(params?: Params) {
|
||||
],
|
||||
});
|
||||
|
||||
await ctx.rebuild();
|
||||
|
||||
global.BUNDLER_CTX = ctx;
|
||||
await global.BUNDLER_CTX.rebuild();
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ import path from "path";
|
||||
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();
|
||||
|
||||
@ -26,7 +27,14 @@ export default function grabArtifactsFromBundledResults({
|
||||
)
|
||||
.filter(([, meta]) => meta.entryPoint)
|
||||
.map(([outputPath, meta]) => {
|
||||
const entrypoint = path.join(ROOT_DIR, meta.entryPoint || "");
|
||||
const entrypoint = meta.entryPoint?.match(/^hydration-virtual:/)
|
||||
? meta.entryPoint?.replace(/^hydration-virtual:/, "")
|
||||
: 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);
|
||||
|
||||
@ -42,7 +50,7 @@ export default function grabArtifactsFromBundledResults({
|
||||
type: outputPath.endsWith(".css")
|
||||
? "text/css"
|
||||
: "text/javascript",
|
||||
entrypoint,
|
||||
entrypoint: meta.entryPoint,
|
||||
css_path: meta.cssBundle,
|
||||
file_name,
|
||||
local_path,
|
||||
|
||||
@ -31,7 +31,7 @@ export default function esbuildCTXArtifactTracker({
|
||||
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();
|
||||
global.RECOMPILING = false;
|
||||
}
|
||||
});
|
||||
|
||||
@ -52,6 +52,8 @@ export default function esbuildCTXArtifactTracker({
|
||||
entryToPage,
|
||||
});
|
||||
|
||||
// console.log("artifacts", artifacts);
|
||||
|
||||
if (artifacts?.[0] && artifacts.length > 0) {
|
||||
for (let i = 0; i < artifacts.length; i++) {
|
||||
const artifact = artifacts[i];
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import type { Plugin } from "esbuild";
|
||||
import path from "path";
|
||||
import type { PageFiles } from "../../../types";
|
||||
import { log } from "../../../utils/log";
|
||||
|
||||
type Params = {
|
||||
entryToPage: Map<
|
||||
|
||||
@ -52,6 +52,8 @@ export default async function watcherEsbuildCTX() {
|
||||
if (global.RECOMPILING) return;
|
||||
global.RECOMPILING = true;
|
||||
|
||||
log.info(`Rebuilding CTX ...`);
|
||||
|
||||
await global.BUNDLER_CTX?.rebuild();
|
||||
|
||||
if (filename.match(/(404|500)\.tsx?/)) {
|
||||
@ -105,12 +107,18 @@ async function fullRebuild(params?: { msg?: string }) {
|
||||
log.watch(msg);
|
||||
}
|
||||
|
||||
log.info(`Reloading Router ...`);
|
||||
|
||||
global.ROUTER.reload();
|
||||
|
||||
log.info(`Disposing Bundler CTX ...`);
|
||||
await global.BUNDLER_CTX?.dispose();
|
||||
global.BUNDLER_CTX = undefined;
|
||||
|
||||
await allPagesESBuildContextBundler({
|
||||
global.BUNDLER_CTX_MAP = {};
|
||||
|
||||
log.info(`Rebuilding Modules ...`);
|
||||
allPagesESBuildContextBundler({
|
||||
post_build_fn: serverPostBuildFn,
|
||||
});
|
||||
} catch (error: any) {
|
||||
@ -120,6 +128,7 @@ async function fullRebuild(params?: { msg?: string }) {
|
||||
}
|
||||
|
||||
if (global.PAGES_SRC_WATCHER) {
|
||||
log.info(`Restarting watcher ...`);
|
||||
global.PAGES_SRC_WATCHER.close();
|
||||
watcherEsbuildCTX();
|
||||
}
|
||||
|
||||
@ -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`;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user