Compare commits

..

3 Commits

Author SHA1 Message Date
321c8ebb89 Refactor Bundler. Fix bugs. 2026-03-24 22:03:55 +01:00
336fa812a5 Update bundler logic 2026-03-24 21:37:41 +01:00
f6c7f6b78c Refactor bundler function 2026-03-24 20:42:47 +01:00
38 changed files with 836 additions and 354 deletions

View File

@ -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.

View File

@ -64,7 +64,6 @@ export default async function allPagesBundler(params) {
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);
process.exit(1);
} }
}); });
// build.onEnd((result) => { // build.onEnd((result) => {

View File

@ -0,0 +1,7 @@
type Params = {
post_build_fn?: (params: {
artifacts: any[];
}) => Promise<void> | void;
};
export default function allPagesESBuildContextBundlerFiles(params?: Params): Promise<void>;
export {};

View File

@ -0,0 +1,58 @@
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 path from "path";
import esbuildCTXArtifactTracker from "./plugins/esbuild-ctx-artifact-tracker";
const { HYDRATION_DST_DIR, BUNX_HYDRATION_SRC_DIR } = grabDirNames();
export default async function allPagesESBuildContextBundlerFiles(params) {
const pages = grabAllPages({ exclude_api: true });
global.PAGE_FILES = pages;
const dev = isDevelopment();
const entryToPage = new Map();
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;
}

View File

@ -2,81 +2,30 @@ import * as esbuild from "esbuild";
import grabAllPages from "../../utils/grab-all-pages"; import grabAllPages from "../../utils/grab-all-pages";
import grabDirNames from "../../utils/grab-dir-names"; import grabDirNames from "../../utils/grab-dir-names";
import isDevelopment from "../../utils/is-development"; import isDevelopment from "../../utils/is-development";
import { log } from "../../utils/log";
import tailwindEsbuildPlugin from "../server/web-pages/tailwind-esbuild-plugin"; import tailwindEsbuildPlugin from "../server/web-pages/tailwind-esbuild-plugin";
import grabClientHydrationScript from "./grab-client-hydration-script"; import grabClientHydrationScript from "./grab-client-hydration-script";
import grabArtifactsFromBundledResults from "./grab-artifacts-from-bundled-result";
import { writeFileSync } from "fs";
import path from "path"; import path from "path";
const { HYDRATION_DST_DIR, HYDRATION_DST_DIR_MAP_JSON_FILE, BUNX_HYDRATION_SRC_DIR, } = grabDirNames(); import virtualFilesPlugin from "./plugins/virtual-files-plugin";
let build_starts = 0; import esbuildCTXArtifactTracker from "./plugins/esbuild-ctx-artifact-tracker";
const MAX_BUILD_STARTS = 10; const { HYDRATION_DST_DIR, BUNX_HYDRATION_SRC_DIR } = grabDirNames();
export default async function allPagesESBuildContextBundler(params) { export default async function allPagesESBuildContextBundler(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;
const dev = isDevelopment(); const dev = isDevelopment();
const entryToPage = new Map(); const entryToPage = new Map();
for (const page of pages) { for (const page of pages) {
const txt = await grabClientHydrationScript({ const tsx = await grabClientHydrationScript({
page_local_path: page.local_path, page_local_path: page.local_path,
}); });
if (!txt) if (!tsx)
continue; continue;
const entryFile = path.join(BUNX_HYDRATION_SRC_DIR, `${page.url_path}.tsx`); const entryFile = path.join(BUNX_HYDRATION_SRC_DIR, `${page.url_path}.tsx`);
await Bun.write(entryFile, txt, { createPath: true }); // await Bun.write(entryFile, txt, { createPath: true });
entryToPage.set(path.resolve(entryFile), page); entryToPage.set(entryFile, { ...page, tsx });
} }
let buildStart = 0; const entryPoints = [...entryToPage.keys()].map((e) => `hydration-virtual:${e}`);
const artifactTracker = { global.BUNDLER_CTX = await esbuild.context({
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({
entryPoints, entryPoints,
outdir: HYDRATION_DST_DIR, outdir: HYDRATION_DST_DIR,
bundle: true, bundle: true,
@ -89,10 +38,21 @@ export default async function allPagesESBuildContextBundler(params) {
}, },
entryNames: "[dir]/[hash]", entryNames: "[dir]/[hash]",
metafile: true, metafile: true,
plugins: [tailwindEsbuildPlugin, artifactTracker], plugins: [
tailwindEsbuildPlugin,
virtualFilesPlugin({
entryToPage,
}),
esbuildCTXArtifactTracker({
entryToPage,
post_build_fn: params?.post_build_fn,
}),
],
jsx: "automatic", jsx: "automatic",
splitting: true, splitting: true,
logLevel: "silent",
// logLevel: "silent", // logLevel: "silent",
// logLevel: dev ? "error" : "silent",
external: [ external: [
"react", "react",
"react-dom", "react-dom",
@ -100,9 +60,5 @@ export default async function allPagesESBuildContextBundler(params) {
"react/jsx-runtime", "react/jsx-runtime",
], ],
}); });
await ctx.rebuild(); await global.BUNDLER_CTX.rebuild();
// if (params?.watch) {
// await ctx.watch();
// }
global.BUNDLER_CTX = ctx;
} }

View File

@ -2,7 +2,9 @@ import * as esbuild from "esbuild";
import type { BundlerCTXMap, PageFiles } from "../../types"; import type { BundlerCTXMap, PageFiles } from "../../types";
type Params = { type Params = {
result: esbuild.BuildResult<esbuild.BuildOptions>; result: esbuild.BuildResult<esbuild.BuildOptions>;
entryToPage: Map<string, PageFiles>; entryToPage: Map<string, PageFiles & {
tsx: string;
}>;
}; };
export default function grabArtifactsFromBundledResults({ result, entryToPage, }: Params): BundlerCTXMap[] | undefined; export default function grabArtifactsFromBundledResults({ result, entryToPage, }: Params): BundlerCTXMap[] | undefined;
export {}; export {};

View File

@ -1,6 +1,7 @@
import path from "path"; import path from "path";
import * as esbuild from "esbuild"; import * as esbuild from "esbuild";
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();
export default function grabArtifactsFromBundledResults({ result, entryToPage, }) { export default function grabArtifactsFromBundledResults({ result, entryToPage, }) {
if (result.errors.length > 0) if (result.errors.length > 0)
@ -8,24 +9,29 @@ export default function grabArtifactsFromBundledResults({ result, entryToPage, }
const artifacts = Object.entries(result.metafile.outputs) const artifacts = Object.entries(result.metafile.outputs)
.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);
if (!target_page || !meta.entryPoint) { if (!target_page || !meta.entryPoint) {
return undefined; return undefined;
} }
const { file_name, local_path, url_path, transformed_path } = target_page; const { file_name, local_path, url_path } = target_page;
return { return {
path: outputPath, path: outputPath,
hash: path.basename(outputPath, path.extname(outputPath)), hash: path.basename(outputPath, path.extname(outputPath)),
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,
url_path, url_path,
transformed_path,
}; };
}); });
if (artifacts.length > 0) { if (artifacts.length > 0) {

View File

@ -1,5 +1,5 @@
type Params = { type Params = {
page_local_path: string; page_local_path: string;
}; };
export default function grabClientHydrationScript({ page_local_path, }: Params): Promise<string>; export default function grabClientHydrationScript({ page_local_path, }: Params): Promise<string | undefined>;
export {}; export {};

View File

@ -13,6 +13,22 @@ export default async function grabClientHydrationScript({ page_local_path, }) {
// const target_root_path = root_file_path // const target_root_path = root_file_path
// ? pagePathTransform({ page_path: root_file_path }) // ? pagePathTransform({ page_path: root_file_path })
// : undefined; // : undefined;
if (!existsSync(page_local_path)) {
return undefined;
}
if (root_file_path) {
if (!existsSync(root_file_path)) {
return undefined;
}
const root_content = await Bun.file(root_file_path).text();
if (!root_content.match(/^export default/m)) {
return undefined;
}
}
const page_content = await Bun.file(page_local_path).text();
if (!page_content.match(/^export default/m)) {
return undefined;
}
let txt = ``; let txt = ``;
txt += `import { hydrateRoot } from "react-dom/client";\n`; txt += `import { hydrateRoot } from "react-dom/client";\n`;
if (root_file_path) { if (root_file_path) {

View File

@ -0,0 +1,12 @@
import type { Plugin } from "esbuild";
import type { PageFiles } from "../../../types";
type Params = {
entryToPage: Map<string, PageFiles & {
tsx: string;
}>;
post_build_fn?: (params: {
artifacts: any[];
}) => Promise<void> | void;
};
export default function esbuildCTXArtifactTracker({ entryToPage, post_build_fn, }: Params): Plugin;
export {};

View File

@ -0,0 +1,57 @@
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;
export default function esbuildCTXArtifactTracker({ entryToPage, post_build_fn, }) {
const artifactTracker = {
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.RECOMPILING = false;
}
});
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,
});
// console.log("artifacts", artifacts);
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;
}

View File

@ -0,0 +1,9 @@
import type { Plugin } from "esbuild";
import type { PageFiles } from "../../../types";
type Params = {
entryToPage: Map<string, PageFiles & {
tsx: string;
}>;
};
export default function virtualFilesPlugin({ entryToPage }: Params): Plugin;
export {};

View File

@ -0,0 +1,28 @@
import path from "path";
import { log } from "../../../utils/log";
export default function virtualFilesPlugin({ entryToPage }) {
const virtualPlugin = {
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;
}

View File

@ -9,7 +9,7 @@ export default async function serverPostBuildFn() {
} }
for (let i = global.HMR_CONTROLLERS.length - 1; i >= 0; i--) { for (let i = global.HMR_CONTROLLERS.length - 1; i >= 0; i--) {
const controller = global.HMR_CONTROLLERS[i]; const controller = global.HMR_CONTROLLERS[i];
if (!controller.target_map?.local_path) { if (!controller?.target_map?.local_path) {
continue; continue;
} }
const target_artifact = global.BUNDLER_CTX_MAP[controller.target_map.local_path]; const target_artifact = global.BUNDLER_CTX_MAP[controller.target_map.local_path];

View File

@ -13,6 +13,9 @@ export default async function watcherEsbuildCTX() {
}, async (event, filename) => { }, async (event, filename) => {
if (!filename) if (!filename)
return; return;
if (filename.match(/^\.\w+/)) {
return;
}
const full_file_path = path.join(ROOT_DIR, filename); const full_file_path = path.join(ROOT_DIR, filename);
if (full_file_path.match(/\/styles$/)) { if (full_file_path.match(/\/styles$/)) {
global.RECOMPILING = true; global.RECOMPILING = true;
@ -38,6 +41,12 @@ export default async function watcherEsbuildCTX() {
return; return;
global.RECOMPILING = true; global.RECOMPILING = true;
await global.BUNDLER_CTX?.rebuild(); 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; return;
} }
@ -69,16 +78,14 @@ async function fullRebuild(params) {
global.ROUTER.reload(); global.ROUTER.reload();
await global.BUNDLER_CTX?.dispose(); await global.BUNDLER_CTX?.dispose();
global.BUNDLER_CTX = undefined; global.BUNDLER_CTX = undefined;
await allPagesESBuildContextBundler({ global.BUNDLER_CTX_MAP = {};
allPagesESBuildContextBundler({
post_build_fn: serverPostBuildFn, post_build_fn: serverPostBuildFn,
}); });
} }
catch (error) { catch (error) {
log.error(error); log.error(error);
} }
finally {
global.RECOMPILING = false;
}
if (global.PAGES_SRC_WATCHER) { if (global.PAGES_SRC_WATCHER) {
global.PAGES_SRC_WATCHER.close(); global.PAGES_SRC_WATCHER.close();
watcherEsbuildCTX(); watcherEsbuildCTX();

View File

@ -6,12 +6,12 @@ import { log } from "../../../utils/log";
import grabRootFilePath from "./grab-root-file-path"; import grabRootFilePath from "./grab-root-file-path";
import grabPageServerRes from "./grab-page-server-res"; import grabPageServerRes from "./grab-page-server-res";
import grabPageServerPath from "./grab-page-server-path"; import grabPageServerPath from "./grab-page-server-path";
import grabPageModules from "./grab-page-modules";
class NotFoundError extends Error { class NotFoundError extends Error {
} }
export default async function grabPageComponent({ req, file_path: passed_file_path, debug, }) { export default async function grabPageComponent({ req, file_path: passed_file_path, debug, }) {
const url = req?.url ? new URL(req.url) : undefined; const url = req?.url ? new URL(req.url) : undefined;
const router = global.ROUTER; const router = global.ROUTER;
const now = Date.now();
let routeParams = undefined; let routeParams = undefined;
try { try {
routeParams = req ? await grabRouteParams({ req }) : undefined; routeParams = req ? await grabRouteParams({ req }) : undefined;
@ -45,63 +45,16 @@ export default async function grabPageComponent({ req, file_path: passed_file_pa
if (debug) { if (debug) {
log.info(`bundledMap:`, bundledMap); log.info(`bundledMap:`, bundledMap);
} }
const { root_file_path } = grabRootFilePath(); const { component, module, serverRes, root_module } = await grabPageModules({
const root_module = 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 = 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 = root_server_fn
? await grabPageServerRes({
server_function: root_server_fn,
url,
query: match?.query,
routeParams,
})
: undefined;
if (debug) {
log.info(`rootServerRes:`, rootServerRes);
}
const module = await import(`${file_path}?t=${now}`);
const { server_file_path } = grabPageServerPath({ file_path });
const server_module = 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 = 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({
file_path, file_path,
root_file_path, debug,
server_res: mergedServerRes, query: match?.query,
})) || {}; routeParams,
if (!component) { url,
throw new Error(`Couldn't grab page component`); });
}
if (debug) {
log.info(`component:`, component);
}
return { return {
component, component,
serverRes: mergedServerRes, serverRes,
routeParams, routeParams,
module, module,
bundledMap, bundledMap,
@ -114,6 +67,7 @@ export default async function grabPageComponent({ req, file_path: passed_file_pa
error, error,
routeParams, routeParams,
is404: error instanceof NotFoundError, is404: error instanceof NotFoundError,
url,
}); });
} }
} }

View File

@ -3,6 +3,7 @@ type Params = {
error?: any; error?: any;
routeParams?: BunxRouteParams; routeParams?: BunxRouteParams;
is404?: boolean; is404?: boolean;
url?: URL;
}; };
export default function grabPageErrorComponent({ error, routeParams, is404, }: Params): Promise<GrabPageComponentRes>; export default function grabPageErrorComponent({ error, routeParams, is404, url, }: Params): Promise<GrabPageComponentRes>;
export {}; export {};

View File

@ -1,31 +1,47 @@
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import grabDirNames from "../../../utils/grab-dir-names"; import grabDirNames from "../../../utils/grab-dir-names";
export default async function grabPageErrorComponent({ error, routeParams, is404, }) { import grabPageModules from "./grab-page-modules";
import _ from "lodash";
export default async function grabPageErrorComponent({ error, routeParams, is404, url, }) {
const router = global.ROUTER; const router = global.ROUTER;
const { BUNX_ROOT_500_PRESET_COMPONENT, BUNX_ROOT_404_PRESET_COMPONENT } = grabDirNames(); const { BUNX_ROOT_500_PRESET_COMPONENT, BUNX_ROOT_404_PRESET_COMPONENT } = grabDirNames();
const errorRoute = is404 ? "/404" : "/500"; const errorRoute = is404 ? "/404" : "/500";
const presetComponent = is404 const presetComponent = is404
? BUNX_ROOT_404_PRESET_COMPONENT ? BUNX_ROOT_404_PRESET_COMPONENT
: BUNX_ROOT_500_PRESET_COMPONENT; : BUNX_ROOT_500_PRESET_COMPONENT;
const default_server_res = {
responseOptions: {
status: is404 ? 404 : 500,
},
};
try { try {
const match = router.match(errorRoute); const match = router.match(errorRoute);
const filePath = match?.filePath || presetComponent; if (!match?.filePath) {
const bundledMap = match?.filePath const default_module = await import(presetComponent);
? global.BUNDLER_CTX_MAP?.[match.filePath] const Component = default_module.default;
: undefined; const default_jsx = (_jsx(Component, { children: _jsx("span", { children: error.message }) }));
const module = await import(filePath); return {
const Component = module.default; component: default_jsx,
const component = _jsx(Component, { children: _jsx("span", { children: error.message }) }); 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 { return {
component, component,
routeParams, routeParams,
module, module,
bundledMap, bundledMap,
serverRes: { serverRes: _.merge(serverRes, default_server_res),
responseOptions: { root_module,
status: is404 ? 404 : 500,
},
},
}; };
} }
catch { catch {
@ -41,12 +57,7 @@ export default async function grabPageErrorComponent({ error, routeParams, is404
component: _jsx(DefaultNotFound, {}), component: _jsx(DefaultNotFound, {}),
routeParams, routeParams,
module: { default: DefaultNotFound }, module: { default: DefaultNotFound },
bundledMap: undefined, serverRes: default_server_res,
serverRes: {
responseOptions: {
status: is404 ? 404 : 500,
},
},
}; };
} }
} }

View File

@ -0,0 +1,16 @@
import type { BunextPageModule, BunextPageModuleServerReturn, BunxRouteParams } from "../../../types";
import type { JSX } from "react";
type Params = {
file_path: string;
debug?: boolean;
url?: URL;
query?: any;
routeParams?: BunxRouteParams;
};
export default function grabPageModules({ file_path, debug, url, query, routeParams, }: Params): Promise<{
component: JSX.Element;
serverRes: BunextPageModuleServerReturn;
module: BunextPageModule;
root_module: BunextPageModule | undefined;
}>;
export {};

View File

@ -0,0 +1,69 @@
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";
export default async function grabPageModules({ file_path, debug, url, query, routeParams, }) {
const now = Date.now();
const { root_file_path } = grabRootFilePath();
const root_module = 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 = 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 = root_server_fn
? await grabPageServerRes({
server_function: root_server_fn,
url,
query,
routeParams,
})
: undefined;
if (debug) {
log.info(`rootServerRes:`, rootServerRes);
}
const module = await import(`${file_path}?t=${now}`);
const { server_file_path } = grabPageServerPath({ file_path });
const server_module = 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 = 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,
};
}

View File

@ -250,7 +250,6 @@ export type GrabPageReactBundledComponentRes = {
}; };
export type PageFiles = { export type PageFiles = {
local_path: string; local_path: string;
transformed_path: string;
url_path: string; url_path: string;
file_name: string; file_name: string;
}; };

View File

@ -67,10 +67,9 @@ function grabPageFileObject({ file_path, }) {
let file_name = url_path.split("/").pop(); let file_name = url_path.split("/").pop();
if (!file_name) if (!file_name)
return; return;
const transformed_path = pagePathTransform({ page_path: file_path }); // const transformed_path = pagePathTransform({ page_path: file_path });
return { return {
local_path: file_path, local_path: file_path,
transformed_path,
url_path, url_path,
file_name, file_name,
}; };

View File

@ -2,7 +2,7 @@
"name": "@moduletrace/bunext", "name": "@moduletrace/bunext",
"module": "index.ts", "module": "index.ts",
"type": "module", "type": "module",
"version": "1.0.32", "version": "1.0.33",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
"exports": { "exports": {

View File

@ -90,7 +90,6 @@ export default async function allPagesBundler(params?: Params) {
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);
process.exit(1);
} }
}); });

View File

@ -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;
}

View File

@ -2,114 +2,51 @@ import * as esbuild from "esbuild";
import grabAllPages from "../../utils/grab-all-pages"; import grabAllPages from "../../utils/grab-all-pages";
import grabDirNames from "../../utils/grab-dir-names"; import grabDirNames from "../../utils/grab-dir-names";
import isDevelopment from "../../utils/is-development"; import isDevelopment from "../../utils/is-development";
import { log } from "../../utils/log";
import tailwindEsbuildPlugin from "../server/web-pages/tailwind-esbuild-plugin"; import tailwindEsbuildPlugin from "../server/web-pages/tailwind-esbuild-plugin";
import grabClientHydrationScript from "./grab-client-hydration-script"; 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 type { PageFiles } from "../../types";
import path from "path"; import path from "path";
import virtualFilesPlugin from "./plugins/virtual-files-plugin";
import esbuildCTXArtifactTracker from "./plugins/esbuild-ctx-artifact-tracker";
const { const { HYDRATION_DST_DIR, BUNX_HYDRATION_SRC_DIR } = grabDirNames();
HYDRATION_DST_DIR,
HYDRATION_DST_DIR_MAP_JSON_FILE,
BUNX_HYDRATION_SRC_DIR,
} = grabDirNames();
let build_starts = 0;
const MAX_BUILD_STARTS = 10;
type Params = { type Params = {
post_build_fn?: (params: { artifacts: any[] }) => Promise<void> | void; post_build_fn?: (params: { artifacts: any[] }) => Promise<void> | void;
// watch?: boolean;
}; };
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;
const dev = isDevelopment(); const dev = isDevelopment();
const entryToPage = new Map<string, PageFiles>(); const entryToPage = new Map<string, PageFiles & { tsx: string }>();
for (const page of pages) { for (const page of pages) {
const txt = await grabClientHydrationScript({ const tsx = await grabClientHydrationScript({
page_local_path: page.local_path, page_local_path: page.local_path,
}); });
if (!txt) continue;
if (!tsx) continue;
const entryFile = path.join( const entryFile = path.join(
BUNX_HYDRATION_SRC_DIR, BUNX_HYDRATION_SRC_DIR,
`${page.url_path}.tsx`, `${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 entryPoints = [...entryToPage.keys()].map(
(e) => `hydration-virtual:${e}`,
);
const artifactTracker: esbuild.Plugin = { global.BUNDLER_CTX = await esbuild.context({
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({
entryPoints, entryPoints,
outdir: HYDRATION_DST_DIR, outdir: HYDRATION_DST_DIR,
bundle: true, bundle: true,
@ -124,10 +61,21 @@ export default async function allPagesESBuildContextBundler(params?: Params) {
}, },
entryNames: "[dir]/[hash]", entryNames: "[dir]/[hash]",
metafile: true, metafile: true,
plugins: [tailwindEsbuildPlugin, artifactTracker], plugins: [
tailwindEsbuildPlugin,
virtualFilesPlugin({
entryToPage,
}),
esbuildCTXArtifactTracker({
entryToPage,
post_build_fn: params?.post_build_fn,
}),
],
jsx: "automatic", jsx: "automatic",
splitting: true, splitting: true,
logLevel: "silent",
// logLevel: "silent", // logLevel: "silent",
// logLevel: dev ? "error" : "silent",
external: [ external: [
"react", "react",
"react-dom", "react-dom",
@ -136,11 +84,5 @@ export default async function allPagesESBuildContextBundler(params?: Params) {
], ],
}); });
await ctx.rebuild(); await global.BUNDLER_CTX.rebuild();
// if (params?.watch) {
// await ctx.watch();
// }
global.BUNDLER_CTX = ctx;
} }

View File

@ -2,12 +2,18 @@ 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();
type Params = { type Params = {
result: esbuild.BuildResult<esbuild.BuildOptions>; result: esbuild.BuildResult<esbuild.BuildOptions>;
entryToPage: Map<string, PageFiles>; entryToPage: Map<
string,
PageFiles & {
tsx: string;
}
>;
}; };
export default function grabArtifactsFromBundledResults({ export default function grabArtifactsFromBundledResults({
@ -21,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);
@ -29,8 +42,7 @@ export default function grabArtifactsFromBundledResults({
return undefined; return undefined;
} }
const { file_name, local_path, url_path, transformed_path } = const { file_name, local_path, url_path } = target_page;
target_page;
return { return {
path: outputPath, path: outputPath,
@ -38,12 +50,11 @@ 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,
url_path, url_path,
transformed_path,
}; };
}); });

View File

@ -28,6 +28,28 @@ export default async function grabClientHydrationScript({
// ? pagePathTransform({ page_path: root_file_path }) // ? pagePathTransform({ page_path: root_file_path })
// : undefined; // : undefined;
if (!existsSync(page_local_path)) {
return undefined;
}
if (root_file_path) {
if (!existsSync(root_file_path)) {
return undefined;
}
const root_content = await Bun.file(root_file_path).text();
if (!root_content.match(/^export default/m)) {
return undefined;
}
}
const page_content = await Bun.file(page_local_path).text();
if (!page_content.match(/^export default/m)) {
return undefined;
}
let txt = ``; let txt = ``;
txt += `import { hydrateRoot } from "react-dom/client";\n`; txt += `import { hydrateRoot } from "react-dom/client";\n`;

View File

@ -0,0 +1,85 @@
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> | 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.RECOMPILING = false;
}
});
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,
});
// console.log("artifacts", artifacts);
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;
}

View File

@ -0,0 +1,46 @@
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 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;
}

View File

@ -14,7 +14,7 @@ export default async function serverPostBuildFn() {
for (let i = global.HMR_CONTROLLERS.length - 1; i >= 0; i--) { for (let i = global.HMR_CONTROLLERS.length - 1; i >= 0; i--) {
const controller = global.HMR_CONTROLLERS[i]; const controller = global.HMR_CONTROLLERS[i];
if (!controller.target_map?.local_path) { if (!controller?.target_map?.local_path) {
continue; continue;
} }

View File

@ -18,6 +18,10 @@ export default async function watcherEsbuildCTX() {
async (event, filename) => { async (event, filename) => {
if (!filename) return; if (!filename) return;
if (filename.match(/^\.\w+/)) {
return;
}
const full_file_path = path.join(ROOT_DIR, filename); const full_file_path = path.join(ROOT_DIR, filename);
if (full_file_path.match(/\/styles$/)) { if (full_file_path.match(/\/styles$/)) {
@ -47,7 +51,21 @@ export default async function watcherEsbuildCTX() {
if (filename.match(target_files_match)) { if (filename.match(target_files_match)) {
if (global.RECOMPILING) return; if (global.RECOMPILING) return;
global.RECOMPILING = true; global.RECOMPILING = true;
await global.BUNDLER_CTX?.rebuild(); 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; return;
} }
@ -92,13 +110,13 @@ async function fullRebuild(params?: { msg?: string }) {
await global.BUNDLER_CTX?.dispose(); await global.BUNDLER_CTX?.dispose();
global.BUNDLER_CTX = undefined; global.BUNDLER_CTX = undefined;
await allPagesESBuildContextBundler({ global.BUNDLER_CTX_MAP = {};
allPagesESBuildContextBundler({
post_build_fn: serverPostBuildFn, post_build_fn: serverPostBuildFn,
}); });
} catch (error: any) { } catch (error: any) {
log.error(error); log.error(error);
} finally {
global.RECOMPILING = false;
} }
if (global.PAGES_SRC_WATCHER) { if (global.PAGES_SRC_WATCHER) {

View File

@ -1,3 +1,4 @@
import type { JSX } from "react";
import type { GrabPageReactBundledComponentRes } from "../../../types"; import type { GrabPageReactBundledComponentRes } from "../../../types";
import grabPageReactComponentString from "./grab-page-react-component-string"; import grabPageReactComponentString from "./grab-page-react-component-string";
import grabTsxStringModule from "./grab-tsx-string-module"; import grabTsxStringModule from "./grab-tsx-string-module";

View File

@ -14,6 +14,7 @@ import { log } from "../../../utils/log";
import grabRootFilePath from "./grab-root-file-path"; import grabRootFilePath from "./grab-root-file-path";
import grabPageServerRes from "./grab-page-server-res"; import grabPageServerRes from "./grab-page-server-res";
import grabPageServerPath from "./grab-page-server-path"; import grabPageServerPath from "./grab-page-server-path";
import grabPageModules from "./grab-page-modules";
class NotFoundError extends Error {} class NotFoundError extends Error {}
@ -30,7 +31,6 @@ export default async function grabPageComponent({
}: Params): Promise<GrabPageComponentRes> { }: Params): Promise<GrabPageComponentRes> {
const url = req?.url ? new URL(req.url) : undefined; const url = req?.url ? new URL(req.url) : undefined;
const router = global.ROUTER; const router = global.ROUTER;
const now = Date.now();
let routeParams: BunxRouteParams | undefined = undefined; let routeParams: BunxRouteParams | undefined = undefined;
@ -78,79 +78,18 @@ export default async function grabPageComponent({
log.info(`bundledMap:`, bundledMap); log.info(`bundledMap:`, bundledMap);
} }
const { root_file_path } = grabRootFilePath(); const { component, module, serverRes, root_module } =
const root_module: BunextRootModule | undefined = root_file_path await grabPageModules({
? 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({
file_path, file_path,
root_file_path, debug,
server_res: mergedServerRes, query: match?.query,
})) || {}; routeParams,
url,
if (!component) { });
throw new Error(`Couldn't grab page component`);
}
if (debug) {
log.info(`component:`, component);
}
return { return {
component, component,
serverRes: mergedServerRes, serverRes,
routeParams, routeParams,
module, module,
bundledMap, bundledMap,
@ -163,6 +102,7 @@ export default async function grabPageComponent({
error, error,
routeParams, routeParams,
is404: error instanceof NotFoundError, is404: error instanceof NotFoundError,
url,
}); });
} }
} }

View File

@ -5,17 +5,21 @@ import type {
BunxRouteParams, BunxRouteParams,
GrabPageComponentRes, GrabPageComponentRes,
} from "../../../types"; } from "../../../types";
import grabPageModules from "./grab-page-modules";
import _ from "lodash";
type Params = { type Params = {
error?: any; error?: any;
routeParams?: BunxRouteParams; routeParams?: BunxRouteParams;
is404?: boolean; is404?: boolean;
url?: URL;
}; };
export default async function grabPageErrorComponent({ export default async function grabPageErrorComponent({
error, error,
routeParams, routeParams,
is404, is404,
url,
}: Params): Promise<GrabPageComponentRes> { }: Params): Promise<GrabPageComponentRes> {
const router = global.ROUTER; const router = global.ROUTER;
@ -27,28 +31,51 @@ export default async function grabPageErrorComponent({
? BUNX_ROOT_404_PRESET_COMPONENT ? BUNX_ROOT_404_PRESET_COMPONENT
: BUNX_ROOT_500_PRESET_COMPONENT; : BUNX_ROOT_500_PRESET_COMPONENT;
const default_server_res = {
responseOptions: {
status: is404 ? 404 : 500,
},
};
try { try {
const match = router.match(errorRoute); const match = router.match(errorRoute);
const filePath = match?.filePath || presetComponent;
const bundledMap = match?.filePath if (!match?.filePath) {
? global.BUNDLER_CTX_MAP?.[match.filePath] const default_module: BunextPageModule = await import(
: undefined; presetComponent
);
const Component = default_module.default as FC<any>;
const default_jsx = (
<Component>{<span>{error.message}</span>}</Component>
);
const module: BunextPageModule = await import(filePath); return {
const Component = module.default as FC<any>; component: default_jsx,
const component = <Component>{<span>{error.message}</span>}</Component>; 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 { return {
component, component,
routeParams, routeParams,
module, module,
bundledMap, bundledMap,
serverRes: { serverRes: _.merge(serverRes, default_server_res),
responseOptions: { root_module,
status: is404 ? 404 : 500,
},
} as any,
}; };
} catch { } catch {
const DefaultNotFound: FC = () => ( const DefaultNotFound: FC = () => (
@ -71,12 +98,7 @@ export default async function grabPageErrorComponent({
component: <DefaultNotFound />, component: <DefaultNotFound />,
routeParams, routeParams,
module: { default: DefaultNotFound }, module: { default: DefaultNotFound },
bundledMap: undefined, serverRes: default_server_res,
serverRes: {
responseOptions: {
status: is404 ? 404 : 500,
},
} as any,
}; };
} }
} }

View File

@ -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,
};
}

View File

@ -283,7 +283,6 @@ export type GrabPageReactBundledComponentRes = {
export type PageFiles = { export type PageFiles = {
local_path: string; local_path: string;
transformed_path: string;
url_path: string; url_path: string;
file_name: string; file_name: string;
}; };

View File

@ -90,11 +90,10 @@ function grabPageFileObject({
let file_name = url_path.split("/").pop(); let file_name = url_path.split("/").pop();
if (!file_name) return; if (!file_name) return;
const transformed_path = pagePathTransform({ page_path: file_path }); // const transformed_path = pagePathTransform({ page_path: file_path });
return { return {
local_path: file_path, local_path: file_path,
transformed_path,
url_path, url_path,
file_name, file_name,
}; };