Update bundler logic
This commit is contained in:
parent
f6c7f6b78c
commit
336fa812a5
16
README.md
16
README.md
@ -1,6 +1,6 @@
|
|||||||
# Bunext
|
# 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
|
## 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:
|
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
|
- 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
|
- 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`.
|
1. Loads `bunext.config.ts` and sets `development: true`.
|
||||||
2. Initializes directories (`.bunext/`, `public/pages/`).
|
2. Initializes directories (`.bunext/`, `public/pages/`).
|
||||||
3. Creates a `Bun.FileSystemRouter` pointed at `src/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.
|
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.
|
6. Waits for the first successful bundle.
|
||||||
7. Starts `Bun.serve()`.
|
7. Starts `Bun.serve()`.
|
||||||
@ -934,7 +934,7 @@ Running `bunext dev`:
|
|||||||
Running `bunext build`:
|
Running `bunext build`:
|
||||||
|
|
||||||
1. Sets `NODE_ENV=production`.
|
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`.
|
3. Writes all bundled artifacts to `.bunext/public/pages/` and the artifact map to `.bunext/public/pages/map.json`.
|
||||||
4. Exits.
|
4. Exits.
|
||||||
|
|
||||||
@ -947,11 +947,13 @@ Running `bunext start`:
|
|||||||
|
|
||||||
### Bundler
|
### 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.
|
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) {
|
export default async function allPagesESBuildContextBundler(params?: Params) {
|
||||||
|
// return await allPagesESBuildContextBundlerFiles(params);
|
||||||
|
|
||||||
const pages = grabAllPages({ exclude_api: true });
|
const pages = grabAllPages({ exclude_api: true });
|
||||||
|
|
||||||
global.PAGE_FILES = pages;
|
global.PAGE_FILES = pages;
|
||||||
@ -39,9 +41,11 @@ export default async function allPagesESBuildContextBundler(params?: Params) {
|
|||||||
entryToPage.set(entryFile, { ...page, tsx });
|
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,
|
entryPoints,
|
||||||
outdir: HYDRATION_DST_DIR,
|
outdir: HYDRATION_DST_DIR,
|
||||||
bundle: true,
|
bundle: true,
|
||||||
@ -69,6 +73,7 @@ export default async function allPagesESBuildContextBundler(params?: Params) {
|
|||||||
jsx: "automatic",
|
jsx: "automatic",
|
||||||
splitting: true,
|
splitting: true,
|
||||||
logLevel: "silent",
|
logLevel: "silent",
|
||||||
|
// logLevel: "silent",
|
||||||
// logLevel: dev ? "error" : "silent",
|
// logLevel: dev ? "error" : "silent",
|
||||||
external: [
|
external: [
|
||||||
"react",
|
"react",
|
||||||
@ -78,7 +83,5 @@ export default async function allPagesESBuildContextBundler(params?: Params) {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
await ctx.rebuild();
|
await global.BUNDLER_CTX.rebuild();
|
||||||
|
|
||||||
global.BUNDLER_CTX = ctx;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import path from "path";
|
|||||||
import * as esbuild from "esbuild";
|
import * as esbuild from "esbuild";
|
||||||
import type { BundlerCTXMap, PageFiles } from "../../types";
|
import type { BundlerCTXMap, PageFiles } from "../../types";
|
||||||
import grabDirNames from "../../utils/grab-dir-names";
|
import grabDirNames from "../../utils/grab-dir-names";
|
||||||
|
import { log } from "../../utils/log";
|
||||||
|
|
||||||
const { ROOT_DIR } = grabDirNames();
|
const { ROOT_DIR } = grabDirNames();
|
||||||
|
|
||||||
@ -26,7 +27,14 @@ export default function grabArtifactsFromBundledResults({
|
|||||||
)
|
)
|
||||||
.filter(([, meta]) => meta.entryPoint)
|
.filter(([, meta]) => meta.entryPoint)
|
||||||
.map(([outputPath, meta]) => {
|
.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);
|
const target_page = entryToPage.get(entrypoint);
|
||||||
|
|
||||||
@ -42,7 +50,7 @@ export default function grabArtifactsFromBundledResults({
|
|||||||
type: outputPath.endsWith(".css")
|
type: outputPath.endsWith(".css")
|
||||||
? "text/css"
|
? "text/css"
|
||||||
: "text/javascript",
|
: "text/javascript",
|
||||||
entrypoint,
|
entrypoint: meta.entryPoint,
|
||||||
css_path: meta.cssBundle,
|
css_path: meta.cssBundle,
|
||||||
file_name,
|
file_name,
|
||||||
local_path,
|
local_path,
|
||||||
|
|||||||
@ -31,7 +31,7 @@ export default function esbuildCTXArtifactTracker({
|
|||||||
if (build_starts == MAX_BUILD_STARTS) {
|
if (build_starts == MAX_BUILD_STARTS) {
|
||||||
const error_msg = `Build Failed. Please check all your components and imports.`;
|
const error_msg = `Build Failed. Please check all your components and imports.`;
|
||||||
log.error(error_msg);
|
log.error(error_msg);
|
||||||
global.BUNDLER_CTX?.cancel();
|
global.RECOMPILING = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -52,6 +52,8 @@ export default function esbuildCTXArtifactTracker({
|
|||||||
entryToPage,
|
entryToPage,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// console.log("artifacts", artifacts);
|
||||||
|
|
||||||
if (artifacts?.[0] && artifacts.length > 0) {
|
if (artifacts?.[0] && artifacts.length > 0) {
|
||||||
for (let i = 0; i < artifacts.length; i++) {
|
for (let i = 0; i < artifacts.length; i++) {
|
||||||
const artifact = artifacts[i];
|
const artifact = artifacts[i];
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import type { Plugin } from "esbuild";
|
import type { Plugin } from "esbuild";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import type { PageFiles } from "../../../types";
|
import type { PageFiles } from "../../../types";
|
||||||
|
import { log } from "../../../utils/log";
|
||||||
|
|
||||||
type Params = {
|
type Params = {
|
||||||
entryToPage: Map<
|
entryToPage: Map<
|
||||||
|
|||||||
@ -52,6 +52,8 @@ export default async function watcherEsbuildCTX() {
|
|||||||
if (global.RECOMPILING) return;
|
if (global.RECOMPILING) return;
|
||||||
global.RECOMPILING = true;
|
global.RECOMPILING = true;
|
||||||
|
|
||||||
|
log.info(`Rebuilding CTX ...`);
|
||||||
|
|
||||||
await global.BUNDLER_CTX?.rebuild();
|
await global.BUNDLER_CTX?.rebuild();
|
||||||
|
|
||||||
if (filename.match(/(404|500)\.tsx?/)) {
|
if (filename.match(/(404|500)\.tsx?/)) {
|
||||||
@ -105,12 +107,18 @@ async function fullRebuild(params?: { msg?: string }) {
|
|||||||
log.watch(msg);
|
log.watch(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.info(`Reloading Router ...`);
|
||||||
|
|
||||||
global.ROUTER.reload();
|
global.ROUTER.reload();
|
||||||
|
|
||||||
|
log.info(`Disposing Bundler CTX ...`);
|
||||||
await global.BUNDLER_CTX?.dispose();
|
await global.BUNDLER_CTX?.dispose();
|
||||||
global.BUNDLER_CTX = undefined;
|
global.BUNDLER_CTX = undefined;
|
||||||
|
|
||||||
await allPagesESBuildContextBundler({
|
global.BUNDLER_CTX_MAP = {};
|
||||||
|
|
||||||
|
log.info(`Rebuilding Modules ...`);
|
||||||
|
allPagesESBuildContextBundler({
|
||||||
post_build_fn: serverPostBuildFn,
|
post_build_fn: serverPostBuildFn,
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@ -120,6 +128,7 @@ async function fullRebuild(params?: { msg?: string }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (global.PAGES_SRC_WATCHER) {
|
if (global.PAGES_SRC_WATCHER) {
|
||||||
|
log.info(`Restarting watcher ...`);
|
||||||
global.PAGES_SRC_WATCHER.close();
|
global.PAGES_SRC_WATCHER.close();
|
||||||
watcherEsbuildCTX();
|
watcherEsbuildCTX();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -41,7 +41,7 @@ export default async function (params?: Params) {
|
|||||||
script += ` try {\n`;
|
script += ` try {\n`;
|
||||||
script += ` document.getElementById("__bunext_error_overlay")?.remove();\n`;
|
script += ` document.getElementById("__bunext_error_overlay")?.remove();\n`;
|
||||||
script += ` const data = JSON.parse(event.data);\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 += ` if (data.reload) {\n`;
|
||||||
script += ` console.log(\`Root Changes Detected. Reloading Page ...\`);\n`;
|
script += ` console.log(\`Root Changes Detected. Reloading Page ...\`);\n`;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user