Bugfix and major refactor. Update server-side page rendering logic.

This commit is contained in:
Benjamin Toby 2026-03-19 05:26:20 +01:00
parent c2c63a1a99
commit ef53ab5f2a
9 changed files with 323 additions and 22 deletions

View File

@ -6,6 +6,7 @@ export default async function () {
const dirNames = grabDirNames();
execSync(`rm -rf ${dirNames.BUNEXT_CACHE_DIR}`);
execSync(`rm -rf ${dirNames.BUNX_CWD_MODULE_CACHE_DIR}`);
const keys = Object.keys(dirNames) as (keyof ReturnType<
typeof grabDirNames

View File

@ -0,0 +1,63 @@
import isDevelopment from "../../../utils/is-development";
import * as esbuild from "esbuild";
import postcss from "postcss";
import tailwindcss from "@tailwindcss/postcss";
import { readFile } from "fs/promises";
import grabDirNames from "../../../utils/grab-dir-names";
import path from "path";
type Params = {
file_path: string;
};
const tailwindPlugin: esbuild.Plugin = {
name: "tailwindcss",
setup(build) {
build.onLoad({ filter: /\.css$/ }, async (args) => {
const source = await readFile(args.path, "utf-8");
const result = await postcss([tailwindcss()]).process(source, {
from: args.path,
});
return {
contents: result.css,
loader: "css",
};
});
},
};
export default async function grabFilePathModule<T extends any = any>({
file_path,
}: Params): Promise<T> {
const dev = isDevelopment();
const { BUNX_CWD_MODULE_CACHE_DIR } = grabDirNames();
const target_cache_file_path = path.join(
BUNX_CWD_MODULE_CACHE_DIR,
`${path.basename(file_path)}.js`,
);
await esbuild.build({
entryPoints: [file_path],
bundle: true,
format: "esm",
target: "es2020",
platform: "node",
external: ["react", "react-dom"],
minify: true,
define: {
"process.env.NODE_ENV": JSON.stringify(
dev ? "development" : "production",
),
},
metafile: true,
plugins: [tailwindPlugin],
jsx: "automatic",
outfile: target_cache_file_path,
});
Loader.registry.delete(target_cache_file_path);
const module = await import(`${target_cache_file_path}?t=${Date.now()}`);
return module as T;
}

View File

@ -0,0 +1,51 @@
import type { GrabPageReactBundledComponentRes } from "../../../types";
import EJSON from "../../../utils/ejson";
import grabTsxStringModule from "./grab-tsx-string-module";
type Params = {
file_path: string;
root_file?: string;
server_res?: any;
};
export default async function grabPageBundledReactComponent({
file_path,
root_file,
server_res,
}: Params): Promise<GrabPageReactBundledComponentRes | undefined> {
try {
let tsx = ``;
const server_res_json = EJSON.stringify(server_res || {})?.replace(
/"/g,
'\\"',
);
if (root_file) {
tsx += `import Root from "${root_file}"\n`;
}
tsx += `import Page from "${file_path}"\n`;
tsx += `export default function Main() {\n\n`;
tsx += `const props = JSON.parse("${server_res_json}")\n\n`;
tsx += ` return (\n`;
if (root_file) {
tsx += ` <Root {...props}><Page {...props} /></Root>\n`;
} else {
tsx += ` <Page {...props} />\n`;
}
tsx += ` )\n`;
tsx += `}\n`;
const mod = await grabTsxStringModule({ tsx, file_path });
const Main = mod.default;
const component = <Main />;
return {
component,
server_res,
};
} catch (error: any) {
return undefined;
}
}

View File

@ -11,6 +11,7 @@ import path from "path";
import AppNames from "../../../utils/grab-app-names";
import { existsSync } from "fs";
import grabPageErrorComponent from "./grab-page-error-component";
import grabPageBundledReactComponent from "./grab-page-bundled-react-component";
class NotFoundError extends Error {}
@ -63,8 +64,6 @@ export default async function grabPageComponent({
throw new Error(errMsg);
}
// const pageName = grabPageName({ path: file_path });
const root_pages_component_ts_file = `${path.join(PAGES_DIR, AppNames["RootPagesComponentName"])}.ts`;
const root_pages_component_tsx_file = `${path.join(PAGES_DIR, AppNames["RootPagesComponentName"])}.tsx`;
const root_pages_component_js_file = `${path.join(PAGES_DIR, AppNames["RootPagesComponentName"])}.js`;
@ -82,17 +81,7 @@ export default async function grabPageComponent({
const now = Date.now();
const root_module = root_file
? await import(`${root_file}?t=${now}`)
: undefined;
const RootComponent = root_module?.default as FC<any> | undefined;
// const component_file_path = root_module
// ? `${file_path}`
// : `${file_path}?t=${global.LAST_BUILD_TIME ?? 0}`;
const module: BunextPageModule = await import(`${file_path}?t=${now}`);
const module: BunextPageModule = await import(file_path);
const serverRes: BunextPageModuleServerReturn = await (async () => {
try {
@ -124,16 +113,18 @@ export default async function grabPageComponent({
: undefined
: undefined;
const Component = module.default as FC<any>;
const Head = module.Head as FC<any>;
const component = RootComponent ? (
<RootComponent {...serverRes}>
<Component {...serverRes} />
</RootComponent>
) : (
<Component {...serverRes} />
);
const { component } =
(await grabPageBundledReactComponent({
file_path,
root_file,
server_res: serverRes,
})) || {};
if (!component) {
throw new Error(`Couldn't grab page component`);
}
return {
component,
@ -152,3 +143,34 @@ export default async function grabPageComponent({
});
}
}
// let root_module: any;
// if (root_file) {
// if (isDevelopment()) {
// root_module = await grabFilePathModule({
// file_path: root_file,
// });
// } else {
// root_module = root_file ? await import(root_file) : undefined;
// }
// }
// const RootComponent = root_module?.default as FC<any> | undefined;
// let module: BunextPageModule;
// if (isDevelopment()) {
// module = await grabFilePathModule({ file_path });
// } else {
// module = await import(file_path);
// }
// const Component = main_module.default as FC<any>;
// const component = RootComponent ? (
// <RootComponent {...serverRes}>
// <Component {...serverRes} />
// </RootComponent>
// ) : (
// <Component {...serverRes} />
// );

View File

@ -0,0 +1,76 @@
import isDevelopment from "../../../utils/is-development";
import * as esbuild from "esbuild";
import postcss from "postcss";
import tailwindcss from "@tailwindcss/postcss";
import { readFile } from "fs/promises";
import grabDirNames from "../../../utils/grab-dir-names";
import path from "path";
import { execSync } from "child_process";
type Params = {
tsx: string;
file_path: string;
};
const tailwindPlugin: esbuild.Plugin = {
name: "tailwindcss",
setup(build) {
build.onLoad({ filter: /\.css$/ }, async (args) => {
const source = await readFile(args.path, "utf-8");
const result = await postcss([tailwindcss()]).process(source, {
from: args.path,
});
return {
contents: result.css,
loader: "css",
};
});
},
};
export default async function grabTsxStringModule<T extends any = any>({
tsx,
file_path,
}: Params): Promise<T> {
const dev = isDevelopment();
const { BUNX_CWD_MODULE_CACHE_DIR } = grabDirNames();
const trimmed_file_path = file_path
.replace(/.*\/src\/pages\//, "")
.replace(/\.tsx$/, "");
const out_file_path = path.join(
BUNX_CWD_MODULE_CACHE_DIR,
`${trimmed_file_path}.js`,
);
await esbuild.build({
stdin: {
contents: tsx,
resolveDir: process.cwd(),
loader: "tsx",
},
bundle: true,
format: "esm",
target: "es2020",
platform: "node",
external: ["react", "react-dom"],
minify: true,
define: {
"process.env.NODE_ENV": JSON.stringify(
dev ? "development" : "production",
),
},
metafile: true,
plugins: [tailwindPlugin],
jsx: "automatic",
write: true,
outfile: out_file_path,
});
Loader.registry.delete(out_file_path);
const module = await import(`${out_file_path}?t=${Date.now()}`);
return module as T;
}

View File

@ -17,7 +17,17 @@ export default function DefaultServerErrorPage({
}}
>
<h1>500 Internal Server Error</h1>
<span>{children}</span>
<div
style={{
maxWidth: "800px",
overflowWrap: "break-word",
wordBreak: "break-all",
maxHeight: "80vh",
overflowY: "auto",
}}
>
{children}
</div>
</div>
);
}

View File

@ -243,6 +243,11 @@ export type GrabPageComponentRes = {
head?: FC<BunextPageHeadFCProps>;
};
export type GrabPageReactBundledComponentRes = {
component: JSX.Element;
server_res?: BunextPageModuleServerReturn;
};
export type PageFiles = {
local_path: string;
url_path: string;

View File

@ -16,6 +16,10 @@ export default function grabDirNames() {
const CONFIG_FILE = path.join(ROOT_DIR, "bunext.config.ts");
const BUNX_CWD_DIR = path.resolve(ROOT_DIR, ".bunext");
const BUNX_CWD_MODULE_CACHE_DIR = path.resolve(
BUNX_CWD_DIR,
"module-cache",
);
const BUNX_TMP_DIR = path.resolve(BUNX_CWD_DIR, ".tmp");
const BUNX_HYDRATION_SRC_DIR = path.resolve(
BUNX_CWD_DIR,
@ -57,5 +61,6 @@ export default function grabDirNames() {
BUNX_ROOT_404_FILE_NAME,
HYDRATION_DST_DIR_MAP_JSON_FILE,
BUNEXT_CACHE_DIR,
BUNX_CWD_MODULE_CACHE_DIR,
};
}

View File

@ -0,0 +1,68 @@
import { resolve, dirname, extname } from "path";
import { existsSync } from "fs";
const SOURCE_EXTENSIONS = [".tsx", ".ts", ".jsx", ".js"];
function getLoader(filePath: string) {
const ext = extname(filePath).slice(1) as any;
return SOURCE_EXTENSIONS.map((e) => e.slice(1)).includes(ext) ? ext : "js";
}
function tryResolveSync(absPath: string): string | null {
if (existsSync(absPath)) return absPath;
for (const ext of SOURCE_EXTENSIONS) {
const p = absPath + ext;
if (existsSync(p)) return p;
}
for (const ext of SOURCE_EXTENSIONS) {
const p = resolve(absPath, "index" + ext);
if (existsSync(p)) return p;
}
return null;
}
export default function registerDevPlugin() {
Bun.plugin({
name: "bunext-dev-hmr",
setup(build) {
// Intercept absolute-path imports that already carry ?t= (our dynamic imports)
build.onResolve({ filter: /\?t=\d+$/ }, (args) => {
if (args.path.includes("node_modules")) return undefined;
const cleanPath = args.path.replace(/\?t=\d+$/, "");
const resolved = tryResolveSync(cleanPath);
if (!resolved) return undefined;
if (!SOURCE_EXTENSIONS.some((e) => resolved.endsWith(e)))
return undefined;
return {
path: `${resolved}?t=${global.LAST_BUILD_TIME ?? 0}`,
namespace: "bunext-dev",
};
});
// Intercept relative imports from within bunext-dev modules
build.onResolve({ filter: /^\./ }, (args) => {
if (!/\?t=\d+/.test(args.importer)) return undefined;
// Strip "namespace:" prefix (e.g. "bunext-dev:") Bun prepends to importer
const cleanImporter = args.importer
.replace(/^[^/]+:(?=\/)/, "")
.replace(/\?t=\d+$/, "");
const base = resolve(dirname(cleanImporter), args.path);
const resolved = tryResolveSync(base);
if (!resolved) return undefined;
if (!SOURCE_EXTENSIONS.some((e) => resolved.endsWith(e)))
return undefined;
return {
path: `${resolved}?t=${global.LAST_BUILD_TIME ?? 0}`,
namespace: "bunext-dev",
};
});
// Load files in the bunext-dev namespace from disk (async is fine in onLoad)
build.onLoad({ filter: /.*/, namespace: "bunext-dev" }, async (args) => {
const realPath = args.path.replace(/\?t=\d+$/, "");
const source = await Bun.file(realPath).text();
return { contents: source, loader: getLoader(realPath) };
});
},
});
}