Server Refactor

This commit is contained in:
2026-03-24 07:17:03 +01:00
parent 7dd8d87be8
commit 342f56f3f5
14 changed files with 278 additions and 111 deletions
+37 -15
View File
@@ -1,7 +1,5 @@
import { describe, expect, test, beforeAll, afterAll } from "bun:test";
import startServer from "../../../src/functions/server/start-server";
import rewritePagesModule from "../../../src/utils/rewrite-pages-module";
import pagePathTransform from "../../../src/utils/page-path-transform";
import path from "path";
import fs from "fs";
@@ -10,9 +8,6 @@ const fixtureDir = path.resolve(__dirname, "../../../test/e2e-fixture");
const fixturePagesDir = path.join(fixtureDir, "src", "pages");
const fixtureIndexPage = path.join(fixturePagesDir, "index.tsx");
// The rewritten page path (inside .bunext/pages, stripped of server logic)
const rewrittenIndexPage = pagePathTransform({ page_path: fixtureIndexPage });
let originalCwd = process.cwd();
let originalPort: string | undefined;
@@ -37,10 +32,6 @@ describe("E2E Integration", () => {
dir: fixturePagesDir,
});
// Rewrite the fixture page (strips server logic) into .bunext/pages
// so that grab-page-react-component-string can resolve the import
await rewritePagesModule({ page_file_path: fixtureIndexPage });
// Pre-populate the bundler context map so grab-page-component can
// look up the compiled path. The `path` value only needs to be
// present for the guard check; SSR does not require the file to exist.
@@ -70,12 +61,6 @@ describe("E2E Integration", () => {
delete process.env.PORT;
}
// Remove the rewritten page created during setup
const rewrittenDir = path.dirname(rewrittenIndexPage);
if (fs.existsSync(rewrittenDir)) {
fs.rmSync(rewrittenDir, { recursive: true, force: true });
}
// Remove any generated .bunext artifacts from the fixture
const dotBunext = path.join(fixtureDir, ".bunext");
if (fs.existsSync(dotBunext)) {
@@ -102,4 +87,41 @@ describe("E2E Integration", () => {
// Default 404 component is rendered
expect(text).toContain("404");
});
test("server props injected from .server.ts companion file", async () => {
const serverFilePath = path.join(fixturePagesDir, "index.server.ts");
const pageFilePath = fixtureIndexPage;
// Write a temporary .server.ts companion that injects a prop
await Bun.write(serverFilePath, `
import type { BunextPageServerFn } from "../../../../../src/types";
const server: BunextPageServerFn<{ greeting: string }> = async () => {
return { props: { greeting: "Hello from server" } };
};
export default server;
`);
// Add the fixture page to the BUNDLER_CTX_MAP
global.BUNDLER_CTX_MAP[pageFilePath] = {
path: ".bunext/public/pages/index.js",
hash: "index",
type: "text/javascript",
entrypoint: pageFilePath,
local_path: pageFilePath,
url_path: "/",
file_name: "index",
};
const response = await fetch(`http://localhost:${server.port}/`);
expect(response.status).toBe(200);
const html = await response.text();
// __PAGE_PROPS__ should include the prop from the server companion
expect(html).toContain("Hello from server");
// Clean up
fs.unlinkSync(serverFilePath);
});
});
@@ -0,0 +1,69 @@
import { describe, it, expect, beforeAll, afterAll } from "bun:test";
import path from "path";
import fs from "fs";
import grabPageServerPath from "../../../../src/functions/server/web-pages/grab-page-server-path";
const tmpDir = path.join(import.meta.dir, "__tmp_server_path__");
beforeAll(() => {
fs.mkdirSync(tmpDir, { recursive: true });
});
afterAll(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
});
describe("grabPageServerPath", () => {
it("returns undefined when no companion file exists", () => {
const { server_file_path } = grabPageServerPath({
file_path: path.join(tmpDir, "index.tsx"),
});
expect(server_file_path).toBeUndefined();
});
it("resolves .server.ts companion for a .tsx page", () => {
const serverFile = path.join(tmpDir, "profile.server.ts");
fs.writeFileSync(serverFile, "export default async () => ({})");
const { server_file_path } = grabPageServerPath({
file_path: path.join(tmpDir, "profile.tsx"),
});
expect(server_file_path).toBe(serverFile);
});
it("resolves .server.tsx companion when only .server.tsx exists", () => {
const serverFile = path.join(tmpDir, "about.server.tsx");
fs.writeFileSync(serverFile, "export default async () => ({})");
const { server_file_path } = grabPageServerPath({
file_path: path.join(tmpDir, "about.tsx"),
});
expect(server_file_path).toBe(serverFile);
});
it("prefers .server.ts over .server.tsx when both exist", () => {
const tsFile = path.join(tmpDir, "blog.server.ts");
const tsxFile = path.join(tmpDir, "blog.server.tsx");
fs.writeFileSync(tsFile, "export default async () => ({})");
fs.writeFileSync(tsxFile, "export default async () => ({})");
const { server_file_path } = grabPageServerPath({
file_path: path.join(tmpDir, "blog.tsx"),
});
expect(server_file_path).toBe(tsFile);
});
it("resolves companion for a .ts page file", () => {
const serverFile = path.join(tmpDir, "api-page.server.ts");
fs.writeFileSync(serverFile, "export default async () => ({})");
const { server_file_path } = grabPageServerPath({
file_path: path.join(tmpDir, "api-page.ts"),
});
expect(server_file_path).toBe(serverFile);
});
});
-3
View File
@@ -13,9 +13,6 @@ export default function () {
return new Command("build")
.description("Build Project")
.action(async () => {
process.env.NODE_ENV = "production";
process.env.BUILD = "true";
try {
rmSync(HYDRATION_DST_DIR, { recursive: true });
rmSync(BUNX_CWD_PAGES_REWRITE_DIR, { recursive: true });
+2 -2
View File
@@ -2,9 +2,9 @@ import { Command } from "commander";
import startServer from "../../functions/server/start-server";
import { log } from "../../utils/log";
import bunextInit from "../../functions/bunext-init";
// import rewritePagesModule from "../../utils/rewrite-pages-module";
import grabDirNames from "../../utils/grab-dir-names";
import { rmSync } from "fs";
import allPagesBunBundler from "../../functions/bundler/all-pages-bun-bundler";
const { HYDRATION_DST_DIR, BUNX_CWD_PAGES_REWRITE_DIR } = grabDirNames();
@@ -21,8 +21,8 @@ export default function () {
rmSync(BUNX_CWD_PAGES_REWRITE_DIR, { recursive: true });
} catch (error) {}
// await rewritePagesModule();
await bunextInit();
await allPagesBunBundler();
await startServer();
});
+3 -2
View File
@@ -2,17 +2,18 @@ import { Command } from "commander";
import startServer from "../../functions/server/start-server";
import { log } from "../../utils/log";
import bunextInit from "../../functions/bunext-init";
import allPagesBunBundler from "../../functions/bundler/all-pages-bun-bundler";
export default function () {
return new Command("start")
.description("Start production server")
.action(async () => {
process.env.NODE_ENV = "production";
log.info("Starting production server ...");
await bunextInit();
await allPagesBunBundler();
await startServer();
});
}
@@ -56,17 +56,19 @@ export default async function allPagesBunBundler(params?: Params) {
const buildStart = performance.now();
const define = {
"process.env.NODE_ENV": JSON.stringify(
dev ? "development" : "production",
),
};
const result = await Bun.build({
entrypoints: [...entryToPage.keys()],
outdir: HYDRATION_DST_DIR,
root: BUNX_HYDRATION_SRC_DIR,
minify: true,
minify: !dev,
format: "esm",
define: {
"process.env.NODE_ENV": JSON.stringify(
dev ? "development" : "production",
),
},
define,
naming: {
entry: "[dir]/[hash].[ext]",
chunk: "chunks/[hash].[ext]",
+4 -4
View File
@@ -25,8 +25,8 @@ export default async function recordArtifacts({
global.BUNDLER_CTX_MAP = _.merge(global.BUNDLER_CTX_MAP, artifacts_map);
}
await Bun.write(
HYDRATION_DST_DIR_MAP_JSON_FILE,
JSON.stringify(artifacts_map, null, 4),
);
// await Bun.write(
// HYDRATION_DST_DIR_MAP_JSON_FILE,
// JSON.stringify(artifacts_map, null, 4),
// );
}
+1 -14
View File
@@ -7,14 +7,12 @@ import type {
} from "../types";
import type { FileSystemRouter, Server } from "bun";
import grabDirNames from "../utils/grab-dir-names";
import { readFileSync, type FSWatcher } from "fs";
import { type FSWatcher } from "fs";
import init from "./init";
import isDevelopment from "../utils/is-development";
import allPagesBundler from "./bundler/all-pages-bundler";
import watcher from "./server/watcher";
import { log } from "../utils/log";
import cron from "./server/cron";
import EJSON from "../utils/ejson";
import allPagesBunBundler from "./bundler/all-pages-bun-bundler";
/**
@@ -36,7 +34,6 @@ declare global {
var PAGE_FILES: PageFiles[];
var ROOT_FILE_UPDATED: boolean;
var SKIPPED_BROWSER_MODULES: Set<string>;
// var BUNDLER_CTX: BuildContext | undefined;
}
const { PAGES_DIR, HYDRATION_DST_DIR_MAP_JSON_FILE } = grabDirNames();
@@ -63,18 +60,8 @@ export default async function bunextInit() {
const is_dev = isDevelopment();
if (is_dev) {
// await allPagesBundler();
await allPagesBunBundler();
watcher();
} else {
const artifacts = EJSON.parse(
readFileSync(HYDRATION_DST_DIR_MAP_JSON_FILE, "utf-8"),
) as { [k: string]: BundlerCTXMap } | undefined;
if (!artifacts) {
log.error("Please build first.");
process.exit(1);
}
global.BUNDLER_CTX_MAP = artifacts;
cron();
}
}
+5 -4
View File
@@ -82,9 +82,9 @@ async function fullRebuild(params?: { msg?: string }) {
global.RECOMPILING = true;
const target_file_paths = global.HMR_CONTROLLERS.map(
(hmr) => hmr.target_map?.local_path,
).filter((f) => typeof f == "string");
// const target_file_paths = global.HMR_CONTROLLERS.map(
// (hmr) => hmr.target_map?.local_path,
// ).filter((f) => typeof f == "string");
// await rewritePagesModule();
@@ -92,7 +92,8 @@ async function fullRebuild(params?: { msg?: string }) {
log.watch(msg);
}
await rebuildBundler({ target_file_paths });
await rebuildBundler();
// await rebuildBundler({ target_file_paths });
} catch (error: any) {
log.error(error);
} finally {
@@ -69,23 +69,22 @@ export default async function genWebHTML({
const dev = isDevelopment();
const devSuffix = dev ? "?dev" : "";
const browser_imports: Record<string, string> = {
react: `https://esm.sh/react@${_reactVersion}`,
"react-dom": `https://esm.sh/react-dom@${_reactVersion}`,
"react-dom/client": `https://esm.sh/react-dom@${_reactVersion}/client`,
"react/jsx-runtime": `https://esm.sh/react@${_reactVersion}/jsx-runtime`,
};
if (dev) {
browser_imports["react/jsx-dev-runtime"] =
`https://esm.sh/react@${_reactVersion}/jsx-dev-runtime`;
}
const importMap = JSON.stringify({
imports: {
react: `https://esm.sh/react@${_reactVersion}${devSuffix}`,
"react-dom": `https://esm.sh/react-dom@${_reactVersion}${devSuffix}`,
"react-dom/client": `https://esm.sh/react-dom@${_reactVersion}/client${devSuffix}`,
"react/jsx-runtime": `https://esm.sh/react@${_reactVersion}/jsx-runtime${devSuffix}`,
"react/jsx-dev-runtime": `https://esm.sh/react@${_reactVersion}/jsx-dev-runtime${devSuffix}`,
},
imports: browser_imports,
});
// let skipped_modules_import_map: { [k: string]: string } = {};
// [...global.SKIPPED_BROWSER_MODULES].forEach((sk) => {
// skipped_modules_import_map[sk] =
// "data:text/javascript,export default {}";
// });
let final_component = (
<html {...html_props}>
<head>
@@ -151,12 +150,32 @@ export default async function genWebHTML({
{Head ? <Head serverRes={pageProps} ctx={routeParams} /> : null}
</head>
<body>
<div id={ClientRootElementIDName}>{component}</div>
<div
id={ClientRootElementIDName}
suppressHydrationWarning={!dev}
>
{component}
</div>
</body>
</html>
);
let html = `<!DOCTYPE html>\n`;
// const stream = await renderToReadableStream(final_component, {
// onError(error: any) {
// // This is where you "omit" or handle the errors
// // You can log it silently or ignore it
// if (error.message.includes('unique "key" prop')) return;
// console.error(error);
// },
// });
// // 2. Convert the Web Stream to a String (Bun-optimized)
// const htmlBody = await new Response(stream).text();
// html += htmlBody;
html += renderToString(final_component);
return html;
+1
View File
@@ -4,6 +4,7 @@ import AppNames from "./grab-app-names";
const prefix = {
info: chalk.bgCyan.bold(" nfo "),
success: chalk.green.bold("✓"),
zap: chalk.green.bold("⚡"),
error: chalk.red.bold("✗"),
warn: chalk.yellow.bold("⚠"),
build: chalk.magenta.bold("⚙"),