Major Bugfix. Fix server component client compatibility

This commit is contained in:
Benjamin Toby 2026-03-22 10:34:30 +01:00
parent 9f619c8898
commit a9af20a8b2
39 changed files with 508 additions and 79 deletions

2
.gitignore vendored
View File

@ -178,4 +178,4 @@ out
/build /build
__fixtures__ __fixtures__
/public /public
/.data /.*

View File

@ -485,12 +485,12 @@ Create `src/pages/__root.tsx` to wrap every page in a shared layout. The root co
```tsx ```tsx
// src/pages/__root.tsx // src/pages/__root.tsx
import type { PropsWithChildren } from "react"; import type { BunextRootComponentProps } from "@moduletrace/bunext/types";
export default function RootLayout({ export default function RootLayout({
children, children,
props, props,
}: PropsWithChildren<any>) { }: BunextRootComponentProps) {
return ( return (
<> <>
<header>My App</header> <header>My App</header>

View File

@ -6,6 +6,10 @@
"name": "bun-next", "name": "bun-next",
"dependencies": { "dependencies": {
"@tailwindcss/postcss": "^4.2.2", "@tailwindcss/postcss": "^4.2.2",
"@types/bun": "latest",
"@types/node": "^24.10.0",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"bun-plugin-tailwind": "^0.1.2", "bun-plugin-tailwind": "^0.1.2",
"chalk": "^5.6.2", "chalk": "^5.6.2",
"commander": "^14.0.2", "commander": "^14.0.2",
@ -19,12 +23,8 @@
"devDependencies": { "devDependencies": {
"@testing-library/dom": "^10.4.1", "@testing-library/dom": "^10.4.1",
"@testing-library/react": "^16.3.2", "@testing-library/react": "^16.3.2",
"@types/bun": "latest",
"@types/lodash": "^4.17.24", "@types/lodash": "^4.17.24",
"@types/micromatch": "^4.0.10", "@types/micromatch": "^4.0.10",
"@types/node": "^24.10.0",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"happy-dom": "^20.8.4", "happy-dom": "^20.8.4",
"react": "^19.2.4", "react": "^19.2.4",
"react-dom": "^19.2.4", "react-dom": "^19.2.4",
@ -169,7 +169,7 @@
"@types/braces": ["@types/braces@3.0.5", "", {}, "sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w=="], "@types/braces": ["@types/braces@3.0.5", "", {}, "sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w=="],
"@types/bun": ["@types/bun@1.2.3", "", { "dependencies": { "bun-types": "1.2.3" } }, "sha512-054h79ipETRfjtsCW9qJK8Ipof67Pw9bodFWmkfkaUaRiIQ1dIV2VTlheshlBx3mpKr0KeK8VqnMMCtgN9rQtw=="], "@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="],
"@types/lodash": ["@types/lodash@4.17.24", "", {}, "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ=="], "@types/lodash": ["@types/lodash@4.17.24", "", {}, "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ=="],
@ -197,7 +197,7 @@
"bun-plugin-tailwind": ["bun-plugin-tailwind@0.1.2", "", { "peerDependencies": { "bun": ">=1.0.0" } }, "sha512-41jNC1tZRSK3s1o7pTNrLuQG8kL/0vR/JgiTmZAJ1eHwe0w5j6HFPKeqEk0WAD13jfrUC7+ULuewFBBCoADPpg=="], "bun-plugin-tailwind": ["bun-plugin-tailwind@0.1.2", "", { "peerDependencies": { "bun": ">=1.0.0" } }, "sha512-41jNC1tZRSK3s1o7pTNrLuQG8kL/0vR/JgiTmZAJ1eHwe0w5j6HFPKeqEk0WAD13jfrUC7+ULuewFBBCoADPpg=="],
"bun-types": ["bun-types@1.2.3", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-P7AeyTseLKAvgaZqQrvp3RqFM3yN9PlcLuSTe7SoJOfZkER73mLdT2vEQi8U64S1YvM/ldcNiQjn0Sn7H9lGgg=="], "bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="],
"chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
@ -339,14 +339,8 @@
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"bun-types/@types/node": ["@types/node@22.13.5", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg=="],
"bun-types/@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="],
"lightningcss-wasm/napi-wasm": ["napi-wasm@1.1.3", "", { "bundled": true }, "sha512-h/4nMGsHjZDCYmQVNODIrYACVJ+I9KItbG+0si6W/jSjdA9JbWDoU4LLeMXVcEQGHjttI2tuXqDrbGF7qkUHHg=="], "lightningcss-wasm/napi-wasm": ["napi-wasm@1.1.3", "", { "bundled": true }, "sha512-h/4nMGsHjZDCYmQVNODIrYACVJ+I9KItbG+0si6W/jSjdA9JbWDoU4LLeMXVcEQGHjttI2tuXqDrbGF7qkUHHg=="],
"strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], "strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
"bun-types/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
} }
} }

View File

@ -4,6 +4,7 @@ import start from "./start";
import dev from "./dev"; import dev from "./dev";
import build from "./build"; import build from "./build";
import { log } from "../utils/log"; import { log } from "../utils/log";
import rewritePages from "./rewrite-pages";
/** /**
* # Describe Program * # Describe Program
*/ */
@ -17,6 +18,7 @@ program
program.addCommand(dev()); program.addCommand(dev());
program.addCommand(start()); program.addCommand(start());
program.addCommand(build()); program.addCommand(build());
program.addCommand(rewritePages());
/** /**
* # Handle Unavailable Commands * # Handle Unavailable Commands
*/ */

View File

@ -0,0 +1,2 @@
import { Command } from "commander";
export default function (): Command;

16
dist/commands/rewrite-pages/index.js vendored Normal file
View File

@ -0,0 +1,16 @@
import { Command } from "commander";
import { log } from "../../utils/log";
import init from "../../functions/init";
import rewritePagesModule from "../../utils/rewrite-pages-module";
export default function () {
return new Command("rewrite-pages")
.description("Rewrite pages from src to .bunext dir")
.action(async () => {
process.env.NODE_ENV = "production";
process.env.BUILD = "true";
await init();
log.banner();
log.build("Rewriting Pages ...");
await rewritePagesModule();
});
}

View File

@ -1,4 +1,4 @@
import { existsSync, statSync, writeFileSync } from "fs"; import { readFileSync, writeFileSync } from "fs";
import * as esbuild from "esbuild"; 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";
@ -8,7 +8,7 @@ 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 grabArtifactsFromBundledResults from "./grab-artifacts-from-bundled-result";
import path from "path"; import stripServerSideLogic from "./strip-server-side-logic";
const { HYDRATION_DST_DIR, HYDRATION_DST_DIR_MAP_JSON_FILE, ROOT_DIR } = grabDirNames(); const { HYDRATION_DST_DIR, HYDRATION_DST_DIR_MAP_JSON_FILE, ROOT_DIR } = grabDirNames();
let build_starts = 0; let build_starts = 0;
const MAX_BUILD_STARTS = 10; const MAX_BUILD_STARTS = 10;
@ -18,9 +18,11 @@ export default async function allPagesBundler(params) {
const dev = isDevelopment(); const dev = isDevelopment();
for (const page of pages) { for (const page of pages) {
const key = page.local_path; const key = page.local_path;
const txt = grabClientHydrationScript({ const txt = await grabClientHydrationScript({
page_local_path: page.local_path, page_local_path: page.local_path,
}); });
if (!txt)
continue;
virtualEntries[key] = txt; virtualEntries[key] = txt;
} }
const virtualPlugin = { const virtualPlugin = {
@ -35,6 +37,19 @@ export default async function allPagesBundler(params) {
loader: "tsx", loader: "tsx",
resolveDir: process.cwd(), resolveDir: process.cwd(),
})); }));
build.onLoad({ filter: /\.tsx$/ }, (args) => {
if (args.path.includes("node_modules"))
return;
const source = readFileSync(args.path, "utf8");
if (!source.includes("server")) {
return { contents: source, loader: "tsx" };
}
const strippedCode = stripServerSideLogic({ txt_code: source });
return {
contents: strippedCode,
loader: "tsx",
};
});
}, },
}; };
const artifactTracker = { const artifactTracker = {
@ -47,7 +62,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

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

View File

@ -3,9 +3,11 @@ import path from "path";
import grabDirNames from "../../utils/grab-dir-names"; import grabDirNames from "../../utils/grab-dir-names";
import AppNames from "../../utils/grab-app-names"; import AppNames from "../../utils/grab-app-names";
import grabConstants from "../../utils/grab-constants"; import grabConstants from "../../utils/grab-constants";
import pagePathTransform from "../../utils/page-path-transform";
const { PAGES_DIR } = grabDirNames(); const { PAGES_DIR } = grabDirNames();
export default function grabClientHydrationScript({ page_local_path }) { export default async function grabClientHydrationScript({ page_local_path, }) {
const { ClientRootElementIDName, ClientRootComponentWindowName, ClientWindowPagePropsName, } = grabConstants(); const { ClientRootElementIDName, ClientRootComponentWindowName, ClientWindowPagePropsName, } = grabConstants();
const target_path = pagePathTransform({ page_path: page_local_path });
const root_component_path = path.join(PAGES_DIR, `${AppNames["RootPagesComponentName"]}.tsx`); const root_component_path = path.join(PAGES_DIR, `${AppNames["RootPagesComponentName"]}.tsx`);
const does_root_exist = existsSync(root_component_path); const does_root_exist = existsSync(root_component_path);
let txt = ``; let txt = ``;
@ -13,7 +15,7 @@ export default function grabClientHydrationScript({ page_local_path }) {
if (does_root_exist) { if (does_root_exist) {
txt += `import Root from "${root_component_path}";\n`; txt += `import Root from "${root_component_path}";\n`;
} }
txt += `import Page from "${page_local_path}";\n\n`; txt += `import Page from "${target_path}";\n\n`;
txt += `const pageProps = window.${ClientWindowPagePropsName} || {};\n`; txt += `const pageProps = window.${ClientWindowPagePropsName} || {};\n`;
if (does_root_exist) { if (does_root_exist) {
txt += `const component = <Root suppressHydrationWarning={true} {...pageProps}><Page {...pageProps} /></Root>\n`; txt += `const component = <Root suppressHydrationWarning={true} {...pageProps}><Page {...pageProps} /></Root>\n`;

View File

@ -0,0 +1,5 @@
type Params = {
txt_code: string;
};
export default function stripServerSideLogic({ txt_code }: Params): string;
export {};

View File

@ -0,0 +1,61 @@
import ts from "typescript";
export default function stripServerSideLogic({ txt_code }) {
const sourceFile = ts.createSourceFile("temp.tsx", txt_code, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
const transformer = (context) => {
return (rootNode) => {
const visitor = (node) => {
if (ts.isVariableStatement(node) &&
node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) {
const isServerExport = node.declarationList.declarations.some((d) => ts.isIdentifier(d.name) &&
d.name.text === "server");
if (isServerExport)
return undefined; // Remove it
}
return ts.visitEachChild(node, visitor, context);
};
return ts.visitNode(rootNode, visitor);
};
};
const result = ts.transform(sourceFile, [transformer]);
const printer = ts.createPrinter();
const strippedCode = printer.printFile(result.transformed[0]);
const cleanSourceFile = ts.createSourceFile("clean.tsx", strippedCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
// Simple reference check: if a named import isn't found in the text, drop it
const cleanupTransformer = (context) => {
return (rootNode) => {
const visitor = (node) => {
if (ts.isImportDeclaration(node)) {
const clause = node.importClause;
if (!clause)
return node;
// Handle named imports like { BunextPageProps, BunextPageServerFn }
if (clause.namedBindings &&
ts.isNamedImports(clause.namedBindings)) {
const activeElements = clause.namedBindings.elements.filter((el) => {
const name = el.name.text;
// Check if the name appears anywhere else in the file
const regex = new RegExp(`\\b${name}\\b`, "g");
const matches = strippedCode.match(regex);
return matches && matches.length > 1; // 1 for the import itself, >1 for usage
});
if (activeElements.length === 0)
return undefined;
return ts.factory.updateImportDeclaration(node, node.modifiers, ts.factory.updateImportClause(clause, clause.isTypeOnly, clause.name, ts.factory.createNamedImports(activeElements)), node.moduleSpecifier, node.attributes);
}
// Handle default imports like 'import BunSQLite'
if (clause.name) {
const name = clause.name.text;
const regex = new RegExp(`\\b${name}\\b`, "g");
const matches = strippedCode.match(regex);
if (!matches || matches.length <= 1)
return undefined;
}
}
return ts.visitEachChild(node, visitor, context);
};
return ts.visitNode(rootNode, visitor);
};
};
const finalResult = ts.transform(cleanSourceFile, [cleanupTransformer]);
return printer.printFile(finalResult.transformed[0]);
}

View File

@ -9,7 +9,7 @@ import { type FSWatcher } from "fs";
declare global { declare global {
var ORA_SPINNER: Ora; var ORA_SPINNER: Ora;
var CONFIG: BunextConfig; var CONFIG: BunextConfig;
var SERVER: Server | undefined; var SERVER: Server<any> | undefined;
var RECOMPILING: boolean; var RECOMPILING: boolean;
var WATCHER_TIMEOUT: any; var WATCHER_TIMEOUT: any;
var ROUTER: FileSystemRouter; var ROUTER: FileSystemRouter;

View File

@ -1 +1 @@
export default function startServer(): Promise<import("bun").Server>; export default function startServer(): Promise<Bun.Server<undefined>>;

View File

@ -3,6 +3,7 @@ import path from "path";
import grabDirNames from "../../utils/grab-dir-names"; import grabDirNames from "../../utils/grab-dir-names";
import rebuildBundler from "./rebuild-bundler"; import rebuildBundler from "./rebuild-bundler";
import { log } from "../../utils/log"; import { log } from "../../utils/log";
import rewritePagesModule from "../../utils/rewrite-pages-module";
const { ROOT_DIR } = grabDirNames(); const { ROOT_DIR } = grabDirNames();
export default async function watcher() { export default async function watcher() {
await Bun.sleep(1000); await Bun.sleep(1000);
@ -36,6 +37,7 @@ export default async function watcher() {
if (global.RECOMPILING) if (global.RECOMPILING)
return; return;
global.RECOMPILING = true; global.RECOMPILING = true;
await rewritePagesModule({ page_url: full_file_path });
await global.BUNDLER_CTX.rebuild(); await global.BUNDLER_CTX.rebuild();
} }
return; return;

View File

@ -1,25 +1,16 @@
import { jsx as _jsx } from "react/jsx-runtime"; import { jsx as _jsx } from "react/jsx-runtime";
import EJSON from "../../../utils/ejson"; import grabPageReactComponentString from "./grab-page-react-component-string";
import grabTsxStringModule from "./grab-tsx-string-module"; import grabTsxStringModule from "./grab-tsx-string-module";
export default async function grabPageBundledReactComponent({ file_path, root_file, server_res, }) { export default async function grabPageBundledReactComponent({ file_path, root_file, server_res, }) {
try { try {
let tsx = ``; let tsx = grabPageReactComponentString({
const server_res_json = JSON.stringify(EJSON.stringify(server_res || {}) ?? "{}"); file_path,
if (root_file) { root_file,
tsx += `import Root from "${root_file}"\n`; server_res,
});
if (!tsx) {
return undefined;
} }
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 suppressHydrationWarning={true} {...props}><Page {...props} /></Root>\n`;
}
else {
tsx += ` <Page suppressHydrationWarning={true} {...props} />\n`;
}
tsx += ` )\n`;
tsx += `}\n`;
const mod = await grabTsxStringModule({ tsx, file_path }); const mod = await grabTsxStringModule({ tsx, file_path });
const Main = mod.default; const Main = mod.default;
const component = _jsx(Main, {}); const component = _jsx(Main, {});

View File

@ -0,0 +1,7 @@
type Params = {
file_path: string;
root_file?: string;
server_res?: any;
};
export default function grabPageReactComponentString({ file_path, root_file, server_res, }: Params): string | undefined;
export {};

View File

@ -0,0 +1,28 @@
import EJSON from "../../../utils/ejson";
import pagePathTransform from "../../../utils/page-path-transform";
export default function grabPageReactComponentString({ file_path, root_file, server_res, }) {
try {
const target_path = pagePathTransform({ page_path: file_path });
let tsx = ``;
const server_res_json = JSON.stringify(EJSON.stringify(server_res || {}) ?? "{}");
if (root_file) {
tsx += `import Root from "${root_file}"\n`;
}
tsx += `import Page from "${target_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 suppressHydrationWarning={true} {...props}><Page {...props} /></Root>\n`;
}
else {
tsx += ` <Page suppressHydrationWarning={true} {...props} />\n`;
}
tsx += ` )\n`;
tsx += `}\n`;
return tsx;
}
catch (error) {
return undefined;
}
}

View File

@ -1,5 +1,5 @@
import type { MatchedRoute, ServeOptions, Server, WebSocketHandler } from "bun"; import type { MatchedRoute, ServeOptions, Server, WebSocketHandler } from "bun";
import type { FC, JSX, ReactNode } from "react"; import type { FC, JSX, PropsWithChildren, ReactNode } from "react";
export type ServerProps = { export type ServerProps = {
params: Record<string, string>; params: Record<string, string>;
searchParams: Record<string, string>; searchParams: Record<string, string>;
@ -71,7 +71,7 @@ export type BunxRouteParams = {
* Intercept and Transform the response object * Intercept and Transform the response object
*/ */
resTransform?: (res: Response) => Promise<Response> | Response; resTransform?: (res: Response) => Promise<Response> | Response;
server?: Server; server?: Server<any>;
}; };
export interface PostInsertReturn { export interface PostInsertReturn {
fieldCount?: number; fieldCount?: number;
@ -270,3 +270,4 @@ export type BunextCacheFileMeta = {
paradigm: "html" | "json"; paradigm: "html" | "json";
expiry_seconds?: number; expiry_seconds?: number;
}; };
export type BunextRootComponentProps = PropsWithChildren & BunextPageProps;

View File

@ -5,6 +5,7 @@ export default function grabDirNames(): {
API_DIR: string; API_DIR: string;
PUBLIC_DIR: string; PUBLIC_DIR: string;
HYDRATION_DST_DIR: string; HYDRATION_DST_DIR: string;
BUNX_CWD_DIR: string;
BUNX_ROOT_DIR: string; BUNX_ROOT_DIR: string;
CONFIG_FILE: string; CONFIG_FILE: string;
BUNX_TMP_DIR: string; BUNX_TMP_DIR: string;
@ -18,4 +19,5 @@ export default function grabDirNames(): {
HYDRATION_DST_DIR_MAP_JSON_FILE: string; HYDRATION_DST_DIR_MAP_JSON_FILE: string;
BUNEXT_CACHE_DIR: string; BUNEXT_CACHE_DIR: string;
BUNX_CWD_MODULE_CACHE_DIR: string; BUNX_CWD_MODULE_CACHE_DIR: string;
BUNX_CWD_PAGES_REWRITE_DIR: string;
}; };

View File

@ -12,6 +12,7 @@ export default function grabDirNames() {
const CONFIG_FILE = path.join(ROOT_DIR, "bunext.config.ts"); const CONFIG_FILE = path.join(ROOT_DIR, "bunext.config.ts");
const BUNX_CWD_DIR = path.resolve(ROOT_DIR, ".bunext"); const BUNX_CWD_DIR = path.resolve(ROOT_DIR, ".bunext");
const BUNX_CWD_MODULE_CACHE_DIR = path.resolve(BUNX_CWD_DIR, "module-cache"); const BUNX_CWD_MODULE_CACHE_DIR = path.resolve(BUNX_CWD_DIR, "module-cache");
const BUNX_CWD_PAGES_REWRITE_DIR = path.resolve(BUNX_CWD_DIR, "pages");
const BUNX_TMP_DIR = path.resolve(BUNX_CWD_DIR, ".tmp"); const BUNX_TMP_DIR = path.resolve(BUNX_CWD_DIR, ".tmp");
const BUNX_HYDRATION_SRC_DIR = path.resolve(BUNX_CWD_DIR, "client", "hydration-src"); const BUNX_HYDRATION_SRC_DIR = path.resolve(BUNX_CWD_DIR, "client", "hydration-src");
const BUNX_ROOT_DIR = path.resolve(__dirname, "../../"); const BUNX_ROOT_DIR = path.resolve(__dirname, "../../");
@ -28,6 +29,7 @@ export default function grabDirNames() {
API_DIR, API_DIR,
PUBLIC_DIR, PUBLIC_DIR,
HYDRATION_DST_DIR, HYDRATION_DST_DIR,
BUNX_CWD_DIR,
BUNX_ROOT_DIR, BUNX_ROOT_DIR,
CONFIG_FILE, CONFIG_FILE,
BUNX_TMP_DIR, BUNX_TMP_DIR,
@ -41,5 +43,6 @@ export default function grabDirNames() {
HYDRATION_DST_DIR_MAP_JSON_FILE, HYDRATION_DST_DIR_MAP_JSON_FILE,
BUNEXT_CACHE_DIR, BUNEXT_CACHE_DIR,
BUNX_CWD_MODULE_CACHE_DIR, BUNX_CWD_MODULE_CACHE_DIR,
BUNX_CWD_PAGES_REWRITE_DIR,
}; };
} }

View File

@ -1 +1 @@
export default function grabRouter(): import("bun").FileSystemRouter; export default function grabRouter(): Bun.FileSystemRouter;

9
dist/utils/page-path-transform.d.ts vendored Normal file
View File

@ -0,0 +1,9 @@
type Params = {
page_path: string;
};
/**
* # Transform a page path to the destination
* path in the .bunext directory
*/
export default function pagePathTransform({ page_path }: Params): string;
export {};

14
dist/utils/page-path-transform.js vendored Normal file
View File

@ -0,0 +1,14 @@
import path from "path";
import grabDirNames from "./grab-dir-names";
const { ROOT_DIR, BUNX_CWD_PAGES_REWRITE_DIR } = grabDirNames();
/**
* # Transform a page path to the destination
* path in the .bunext directory
*/
export default function pagePathTransform({ page_path }) {
const page_path_relative_dir = page_path
.replace(ROOT_DIR, "")
.replace(/\/src\/pages/, "");
const target_path = path.join(BUNX_CWD_PAGES_REWRITE_DIR, page_path_relative_dir);
return target_path;
}

5
dist/utils/rewrite-pages-module.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
type Params = {
page_url?: string | string[];
};
export default function rewritePagesModule(params?: Params): Promise<void>;
export {};

25
dist/utils/rewrite-pages-module.js vendored Normal file
View File

@ -0,0 +1,25 @@
import grabAllPages from "./grab-all-pages";
import pagePathTransform from "./page-path-transform";
import stripServerSideLogic from "../functions/bundler/strip-server-side-logic";
export default async function rewritePagesModule(params) {
const { page_url } = params || {};
let target_pages;
if (page_url) {
target_pages = Array.isArray(page_url) ? page_url : [page_url];
}
else {
const pages = grabAllPages({ exclude_api: true });
target_pages = pages.map((p) => p.local_path);
}
for (let i = 0; i < target_pages.length; i++) {
const page_path = target_pages[i];
const dst_path = pagePathTransform({ page_path });
const origin_page_content = await Bun.file(page_path).text();
const dst_page_content = stripServerSideLogic({
txt_code: origin_page_content,
});
await Bun.write(dst_path, dst_page_content, {
createPath: true,
});
}
}

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.9", "version": "1.0.10",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
"exports": { "exports": {
@ -51,6 +51,10 @@
}, },
"dependencies": { "dependencies": {
"@tailwindcss/postcss": "^4.2.2", "@tailwindcss/postcss": "^4.2.2",
"@types/bun": "latest",
"@types/node": "^24.10.0",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"bun-plugin-tailwind": "^0.1.2", "bun-plugin-tailwind": "^0.1.2",
"chalk": "^5.6.2", "chalk": "^5.6.2",
"commander": "^14.0.2", "commander": "^14.0.2",
@ -59,10 +63,6 @@
"lodash": "^4.17.23", "lodash": "^4.17.23",
"micromatch": "^4.0.8", "micromatch": "^4.0.8",
"ora": "^9.0.0", "ora": "^9.0.0",
"postcss": "^8.5.8", "postcss": "^8.5.8"
"@types/node": "^24.10.0",
"@types/bun": "latest",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2"
} }
} }

View File

@ -5,6 +5,7 @@ import start from "./start";
import dev from "./dev"; import dev from "./dev";
import build from "./build"; import build from "./build";
import { log } from "../utils/log"; import { log } from "../utils/log";
import rewritePages from "./rewrite-pages";
/** /**
* # Describe Program * # Describe Program
@ -20,6 +21,7 @@ program
program.addCommand(dev()); program.addCommand(dev());
program.addCommand(start()); program.addCommand(start());
program.addCommand(build()); program.addCommand(build());
program.addCommand(rewritePages());
/** /**
* # Handle Unavailable Commands * # Handle Unavailable Commands

View File

@ -0,0 +1,20 @@
import { Command } from "commander";
import { log } from "../../utils/log";
import init from "../../functions/init";
import rewritePagesModule from "../../utils/rewrite-pages-module";
export default function () {
return new Command("rewrite-pages")
.description("Rewrite pages from src to .bunext dir")
.action(async () => {
process.env.NODE_ENV = "production";
process.env.BUILD = "true";
await init();
log.banner();
log.build("Rewriting Pages ...");
await rewritePagesModule();
});
}

View File

@ -1,4 +1,4 @@
import { existsSync, statSync, writeFileSync } from "fs"; import { readFileSync, writeFileSync } from "fs";
import * as esbuild from "esbuild"; 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";
@ -9,7 +9,7 @@ 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 grabArtifactsFromBundledResults from "./grab-artifacts-from-bundled-result";
import path from "path"; import stripServerSideLogic from "./strip-server-side-logic";
const { HYDRATION_DST_DIR, HYDRATION_DST_DIR_MAP_JSON_FILE, ROOT_DIR } = const { HYDRATION_DST_DIR, HYDRATION_DST_DIR_MAP_JSON_FILE, ROOT_DIR } =
grabDirNames(); grabDirNames();
@ -32,10 +32,12 @@ export default async function allPagesBundler(params?: Params) {
for (const page of pages) { for (const page of pages) {
const key = page.local_path; const key = page.local_path;
const txt = grabClientHydrationScript({ const txt = await grabClientHydrationScript({
page_local_path: page.local_path, page_local_path: page.local_path,
}); });
if (!txt) continue;
virtualEntries[key] = txt; virtualEntries[key] = txt;
} }
@ -52,6 +54,23 @@ export default async function allPagesBundler(params?: Params) {
loader: "tsx", loader: "tsx",
resolveDir: process.cwd(), resolveDir: process.cwd(),
})); }));
build.onLoad({ filter: /\.tsx$/ }, (args) => {
if (args.path.includes("node_modules")) return;
const source = readFileSync(args.path, "utf8");
if (!source.includes("server")) {
return { contents: source, loader: "tsx" };
}
const strippedCode = stripServerSideLogic({ txt_code: source });
return {
contents: strippedCode,
loader: "tsx",
};
});
}, },
}; };
@ -67,7 +86,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

@ -3,6 +3,7 @@ import path from "path";
import grabDirNames from "../../utils/grab-dir-names"; import grabDirNames from "../../utils/grab-dir-names";
import AppNames from "../../utils/grab-app-names"; import AppNames from "../../utils/grab-app-names";
import grabConstants from "../../utils/grab-constants"; import grabConstants from "../../utils/grab-constants";
import pagePathTransform from "../../utils/page-path-transform";
const { PAGES_DIR } = grabDirNames(); const { PAGES_DIR } = grabDirNames();
@ -10,13 +11,17 @@ type Params = {
page_local_path: string; page_local_path: string;
}; };
export default function grabClientHydrationScript({ page_local_path }: Params) { export default async function grabClientHydrationScript({
page_local_path,
}: Params) {
const { const {
ClientRootElementIDName, ClientRootElementIDName,
ClientRootComponentWindowName, ClientRootComponentWindowName,
ClientWindowPagePropsName, ClientWindowPagePropsName,
} = grabConstants(); } = grabConstants();
const target_path = pagePathTransform({ page_path: page_local_path });
const root_component_path = path.join( const root_component_path = path.join(
PAGES_DIR, PAGES_DIR,
`${AppNames["RootPagesComponentName"]}.tsx`, `${AppNames["RootPagesComponentName"]}.tsx`,
@ -30,7 +35,7 @@ export default function grabClientHydrationScript({ page_local_path }: Params) {
if (does_root_exist) { if (does_root_exist) {
txt += `import Root from "${root_component_path}";\n`; txt += `import Root from "${root_component_path}";\n`;
} }
txt += `import Page from "${page_local_path}";\n\n`; txt += `import Page from "${target_path}";\n\n`;
txt += `const pageProps = window.${ClientWindowPagePropsName} || {};\n`; txt += `const pageProps = window.${ClientWindowPagePropsName} || {};\n`;
if (does_root_exist) { if (does_root_exist) {

View File

@ -0,0 +1,106 @@
import ts from "typescript";
type Params = {
txt_code: string;
};
export default function stripServerSideLogic({ txt_code }: Params) {
const sourceFile = ts.createSourceFile(
"temp.tsx",
txt_code,
ts.ScriptTarget.Latest,
true,
ts.ScriptKind.TSX,
);
const transformer: ts.TransformerFactory<ts.SourceFile> = (context) => {
return (rootNode) => {
const visitor = (node: ts.Node): ts.Node | undefined => {
if (
ts.isVariableStatement(node) &&
node.modifiers?.some(
(m) => m.kind === ts.SyntaxKind.ExportKeyword,
)
) {
const isServerExport =
node.declarationList.declarations.some(
(d) =>
ts.isIdentifier(d.name) &&
d.name.text === "server",
);
if (isServerExport) return undefined; // Remove it
}
return ts.visitEachChild(node, visitor, context);
};
return ts.visitNode(rootNode, visitor) as ts.SourceFile;
};
};
const result = ts.transform(sourceFile, [transformer]);
const printer = ts.createPrinter();
const strippedCode = printer.printFile(result.transformed[0]);
const cleanSourceFile = ts.createSourceFile(
"clean.tsx",
strippedCode,
ts.ScriptTarget.Latest,
true,
ts.ScriptKind.TSX,
);
// Simple reference check: if a named import isn't found in the text, drop it
const cleanupTransformer: ts.TransformerFactory<ts.SourceFile> = (
context,
) => {
return (rootNode) => {
const visitor = (node: ts.Node): ts.Node | undefined => {
if (ts.isImportDeclaration(node)) {
const clause = node.importClause;
if (!clause) return node;
// Handle named imports like { BunextPageProps, BunextPageServerFn }
if (
clause.namedBindings &&
ts.isNamedImports(clause.namedBindings)
) {
const activeElements =
clause.namedBindings.elements.filter((el) => {
const name = el.name.text;
// Check if the name appears anywhere else in the file
const regex = new RegExp(`\\b${name}\\b`, "g");
const matches = strippedCode.match(regex);
return matches && matches.length > 1; // 1 for the import itself, >1 for usage
});
if (activeElements.length === 0) return undefined;
return ts.factory.updateImportDeclaration(
node,
node.modifiers,
ts.factory.updateImportClause(
clause,
clause.isTypeOnly,
clause.name,
ts.factory.createNamedImports(activeElements),
),
node.moduleSpecifier,
node.attributes,
);
}
// Handle default imports like 'import BunSQLite'
if (clause.name) {
const name = clause.name.text;
const regex = new RegExp(`\\b${name}\\b`, "g");
const matches = strippedCode.match(regex);
if (!matches || matches.length <= 1) return undefined;
}
}
return ts.visitEachChild(node, visitor, context);
};
return ts.visitNode(rootNode, visitor) as ts.SourceFile;
};
};
const finalResult = ts.transform(cleanSourceFile, [cleanupTransformer]);
return printer.printFile(finalResult.transformed[0]);
}

View File

@ -24,7 +24,7 @@ import cron from "./server/cron";
declare global { declare global {
var ORA_SPINNER: Ora; var ORA_SPINNER: Ora;
var CONFIG: BunextConfig; var CONFIG: BunextConfig;
var SERVER: Server | undefined; var SERVER: Server<any> | undefined;
var RECOMPILING: boolean; var RECOMPILING: boolean;
var WATCHER_TIMEOUT: any; var WATCHER_TIMEOUT: any;
var ROUTER: FileSystemRouter; var ROUTER: FileSystemRouter;

View File

@ -3,6 +3,7 @@ import path from "path";
import grabDirNames from "../../utils/grab-dir-names"; import grabDirNames from "../../utils/grab-dir-names";
import rebuildBundler from "./rebuild-bundler"; import rebuildBundler from "./rebuild-bundler";
import { log } from "../../utils/log"; import { log } from "../../utils/log";
import rewritePagesModule from "../../utils/rewrite-pages-module";
const { ROOT_DIR } = grabDirNames(); const { ROOT_DIR } = grabDirNames();
@ -47,6 +48,7 @@ export default async function watcher() {
if (filename.match(target_files_match) && global.BUNDLER_CTX) { if (filename.match(target_files_match) && global.BUNDLER_CTX) {
if (global.RECOMPILING) return; if (global.RECOMPILING) return;
global.RECOMPILING = true; global.RECOMPILING = true;
await rewritePagesModule({ page_url: full_file_path });
await global.BUNDLER_CTX.rebuild(); await global.BUNDLER_CTX.rebuild();
} }
return; return;

View File

@ -1,5 +1,5 @@
import type { GrabPageReactBundledComponentRes } from "../../../types"; import type { GrabPageReactBundledComponentRes } from "../../../types";
import EJSON from "../../../utils/ejson"; import grabPageReactComponentString from "./grab-page-react-component-string";
import grabTsxStringModule from "./grab-tsx-string-module"; import grabTsxStringModule from "./grab-tsx-string-module";
type Params = { type Params = {
@ -14,28 +14,16 @@ export default async function grabPageBundledReactComponent({
server_res, server_res,
}: Params): Promise<GrabPageReactBundledComponentRes | undefined> { }: Params): Promise<GrabPageReactBundledComponentRes | undefined> {
try { try {
let tsx = ``; let tsx = grabPageReactComponentString({
file_path,
root_file,
server_res,
});
const server_res_json = JSON.stringify( if (!tsx) {
EJSON.stringify(server_res || {}) ?? "{}", return undefined;
);
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 suppressHydrationWarning={true} {...props}><Page {...props} /></Root>\n`;
} else {
tsx += ` <Page suppressHydrationWarning={true} {...props} />\n`;
}
tsx += ` )\n`;
tsx += `}\n`;
const mod = await grabTsxStringModule({ tsx, file_path }); const mod = await grabTsxStringModule({ tsx, file_path });
const Main = mod.default; const Main = mod.default;
const component = <Main />; const component = <Main />;

View File

@ -0,0 +1,43 @@
import EJSON from "../../../utils/ejson";
import pagePathTransform from "../../../utils/page-path-transform";
type Params = {
file_path: string;
root_file?: string;
server_res?: any;
};
export default function grabPageReactComponentString({
file_path,
root_file,
server_res,
}: Params): string | undefined {
try {
const target_path = pagePathTransform({ page_path: file_path });
let tsx = ``;
const server_res_json = JSON.stringify(
EJSON.stringify(server_res || {}) ?? "{}",
);
if (root_file) {
tsx += `import Root from "${root_file}"\n`;
}
tsx += `import Page from "${target_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 suppressHydrationWarning={true} {...props}><Page {...props} /></Root>\n`;
} else {
tsx += ` <Page suppressHydrationWarning={true} {...props} />\n`;
}
tsx += ` )\n`;
tsx += `}\n`;
return tsx;
} catch (error: any) {
return undefined;
}
}

View File

@ -1,5 +1,5 @@
import type { MatchedRoute, ServeOptions, Server, WebSocketHandler } from "bun"; import type { MatchedRoute, ServeOptions, Server, WebSocketHandler } from "bun";
import type { FC, JSX, ReactNode } from "react"; import type { FC, JSX, PropsWithChildren, ReactNode } from "react";
export type ServerProps = { export type ServerProps = {
params: Record<string, string>; params: Record<string, string>;
@ -84,7 +84,7 @@ export type BunxRouteParams = {
* Intercept and Transform the response object * Intercept and Transform the response object
*/ */
resTransform?: (res: Response) => Promise<Response> | Response; resTransform?: (res: Response) => Promise<Response> | Response;
server?: Server; server?: Server<any>;
}; };
export interface PostInsertReturn { export interface PostInsertReturn {
@ -293,3 +293,5 @@ export type BunextCacheFileMeta = {
paradigm: "html" | "json"; paradigm: "html" | "json";
expiry_seconds?: number; expiry_seconds?: number;
}; };
export type BunextRootComponentProps = PropsWithChildren & BunextPageProps;

View File

@ -20,6 +20,7 @@ export default function grabDirNames() {
BUNX_CWD_DIR, BUNX_CWD_DIR,
"module-cache", "module-cache",
); );
const BUNX_CWD_PAGES_REWRITE_DIR = path.resolve(BUNX_CWD_DIR, "pages");
const BUNX_TMP_DIR = path.resolve(BUNX_CWD_DIR, ".tmp"); const BUNX_TMP_DIR = path.resolve(BUNX_CWD_DIR, ".tmp");
const BUNX_HYDRATION_SRC_DIR = path.resolve( const BUNX_HYDRATION_SRC_DIR = path.resolve(
BUNX_CWD_DIR, BUNX_CWD_DIR,
@ -49,6 +50,7 @@ export default function grabDirNames() {
API_DIR, API_DIR,
PUBLIC_DIR, PUBLIC_DIR,
HYDRATION_DST_DIR, HYDRATION_DST_DIR,
BUNX_CWD_DIR,
BUNX_ROOT_DIR, BUNX_ROOT_DIR,
CONFIG_FILE, CONFIG_FILE,
BUNX_TMP_DIR, BUNX_TMP_DIR,
@ -62,5 +64,6 @@ export default function grabDirNames() {
HYDRATION_DST_DIR_MAP_JSON_FILE, HYDRATION_DST_DIR_MAP_JSON_FILE,
BUNEXT_CACHE_DIR, BUNEXT_CACHE_DIR,
BUNX_CWD_MODULE_CACHE_DIR, BUNX_CWD_MODULE_CACHE_DIR,
BUNX_CWD_PAGES_REWRITE_DIR,
}; };
} }

View File

@ -0,0 +1,24 @@
import path from "path";
import grabDirNames from "./grab-dir-names";
type Params = {
page_path: string;
};
const { ROOT_DIR, BUNX_CWD_PAGES_REWRITE_DIR } = grabDirNames();
/**
* # Transform a page path to the destination
* path in the .bunext directory
*/
export default function pagePathTransform({ page_path }: Params) {
const page_path_relative_dir = page_path
.replace(ROOT_DIR, "")
.replace(/\/src\/pages/, "");
const target_path = path.join(
BUNX_CWD_PAGES_REWRITE_DIR,
page_path_relative_dir,
);
return target_path;
}

View File

@ -0,0 +1,33 @@
import grabAllPages from "./grab-all-pages";
import pagePathTransform from "./page-path-transform";
import stripServerSideLogic from "../functions/bundler/strip-server-side-logic";
type Params = {
page_url?: string | string[];
};
export default async function rewritePagesModule(params?: Params) {
const { page_url } = params || {};
let target_pages: string[] | undefined;
if (page_url) {
target_pages = Array.isArray(page_url) ? page_url : [page_url];
} else {
const pages = grabAllPages({ exclude_api: true });
target_pages = pages.map((p) => p.local_path);
}
for (let i = 0; i < target_pages.length; i++) {
const page_path = target_pages[i];
const dst_path = pagePathTransform({ page_path });
const origin_page_content = await Bun.file(page_path).text();
const dst_page_content = stripServerSideLogic({
txt_code: origin_page_content,
});
await Bun.write(dst_path, dst_page_content, {
createPath: true,
});
}
}