From a9af20a8b272c075c5b5a2f7c49e53f4cde59635 Mon Sep 17 00:00:00 2001 From: Benjamin Toby Date: Sun, 22 Mar 2026 10:34:30 +0100 Subject: [PATCH] Major Bugfix. Fix server component client compatibility --- .gitignore | 2 +- README.md | 4 +- bun.lock | 18 +-- dist/commands/index.js | 2 + dist/commands/rewrite-pages/index.d.ts | 2 + dist/commands/rewrite-pages/index.js | 16 +++ dist/functions/bundler/all-pages-bundler.js | 22 +++- .../bundler/grab-client-hydration-script.d.ts | 2 +- .../bundler/grab-client-hydration-script.js | 6 +- .../bundler/strip-server-side-logic.d.ts | 5 + .../bundler/strip-server-side-logic.js | 61 ++++++++++ dist/functions/bunext-init.d.ts | 2 +- dist/functions/server/start-server.d.ts | 2 +- dist/functions/server/watcher.js | 2 + .../grab-page-bundled-react-component.js | 25 ++--- .../grab-page-react-component-string.d.ts | 7 ++ .../grab-page-react-component-string.js | 28 +++++ dist/types/index.d.ts | 5 +- dist/utils/grab-dir-names.d.ts | 2 + dist/utils/grab-dir-names.js | 3 + dist/utils/grab-router.d.ts | 2 +- dist/utils/page-path-transform.d.ts | 9 ++ dist/utils/page-path-transform.js | 14 +++ dist/utils/rewrite-pages-module.d.ts | 5 + dist/utils/rewrite-pages-module.js | 25 +++++ package.json | 12 +- src/commands/index.ts | 2 + src/commands/rewrite-pages/index.ts | 20 ++++ src/functions/bundler/all-pages-bundler.ts | 26 ++++- ...pt.ts => grab-client-hydration-script.tsx} | 9 +- .../bundler/strip-server-side-logic.tsx | 106 ++++++++++++++++++ src/functions/bunext-init.ts | 2 +- src/functions/server/watcher.ts | 2 + .../grab-page-bundled-react-component.tsx | 28 ++--- .../grab-page-react-component-string.tsx | 43 +++++++ src/types/index.ts | 6 +- src/utils/grab-dir-names.ts | 3 + src/utils/page-path-transform.ts | 24 ++++ src/utils/rewrite-pages-module.ts | 33 ++++++ 39 files changed, 508 insertions(+), 79 deletions(-) create mode 100644 dist/commands/rewrite-pages/index.d.ts create mode 100644 dist/commands/rewrite-pages/index.js create mode 100644 dist/functions/bundler/strip-server-side-logic.d.ts create mode 100644 dist/functions/bundler/strip-server-side-logic.js create mode 100644 dist/functions/server/web-pages/grab-page-react-component-string.d.ts create mode 100644 dist/functions/server/web-pages/grab-page-react-component-string.js create mode 100644 dist/utils/page-path-transform.d.ts create mode 100644 dist/utils/page-path-transform.js create mode 100644 dist/utils/rewrite-pages-module.d.ts create mode 100644 dist/utils/rewrite-pages-module.js create mode 100644 src/commands/rewrite-pages/index.ts rename src/functions/bundler/{grab-client-hydration-script.ts => grab-client-hydration-script.tsx} (87%) create mode 100644 src/functions/bundler/strip-server-side-logic.tsx create mode 100644 src/functions/server/web-pages/grab-page-react-component-string.tsx create mode 100644 src/utils/page-path-transform.ts create mode 100644 src/utils/rewrite-pages-module.ts diff --git a/.gitignore b/.gitignore index 79473aa..3a15c7b 100644 --- a/.gitignore +++ b/.gitignore @@ -178,4 +178,4 @@ out /build __fixtures__ /public -/.data \ No newline at end of file +/.* \ No newline at end of file diff --git a/README.md b/README.md index 36fec8f..b0a1f24 100644 --- a/README.md +++ b/README.md @@ -485,12 +485,12 @@ Create `src/pages/__root.tsx` to wrap every page in a shared layout. The root co ```tsx // src/pages/__root.tsx -import type { PropsWithChildren } from "react"; +import type { BunextRootComponentProps } from "@moduletrace/bunext/types"; export default function RootLayout({ children, props, -}: PropsWithChildren) { +}: BunextRootComponentProps) { return ( <>
My App
diff --git a/bun.lock b/bun.lock index 0751338..512d189 100644 --- a/bun.lock +++ b/bun.lock @@ -6,6 +6,10 @@ "name": "bun-next", "dependencies": { "@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", "chalk": "^5.6.2", "commander": "^14.0.2", @@ -19,12 +23,8 @@ "devDependencies": { "@testing-library/dom": "^10.4.1", "@testing-library/react": "^16.3.2", - "@types/bun": "latest", "@types/lodash": "^4.17.24", "@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", "react": "^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/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=="], @@ -197,7 +197,7 @@ "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=="], @@ -339,14 +339,8 @@ "@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=="], "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=="], } } diff --git a/dist/commands/index.js b/dist/commands/index.js index df5f98d..924a72b 100644 --- a/dist/commands/index.js +++ b/dist/commands/index.js @@ -4,6 +4,7 @@ import start from "./start"; import dev from "./dev"; import build from "./build"; import { log } from "../utils/log"; +import rewritePages from "./rewrite-pages"; /** * # Describe Program */ @@ -17,6 +18,7 @@ program program.addCommand(dev()); program.addCommand(start()); program.addCommand(build()); +program.addCommand(rewritePages()); /** * # Handle Unavailable Commands */ diff --git a/dist/commands/rewrite-pages/index.d.ts b/dist/commands/rewrite-pages/index.d.ts new file mode 100644 index 0000000..f202e7d --- /dev/null +++ b/dist/commands/rewrite-pages/index.d.ts @@ -0,0 +1,2 @@ +import { Command } from "commander"; +export default function (): Command; diff --git a/dist/commands/rewrite-pages/index.js b/dist/commands/rewrite-pages/index.js new file mode 100644 index 0000000..df83af3 --- /dev/null +++ b/dist/commands/rewrite-pages/index.js @@ -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(); + }); +} diff --git a/dist/functions/bundler/all-pages-bundler.js b/dist/functions/bundler/all-pages-bundler.js index da72dc5..bc8d99a 100644 --- a/dist/functions/bundler/all-pages-bundler.js +++ b/dist/functions/bundler/all-pages-bundler.js @@ -1,4 +1,4 @@ -import { existsSync, statSync, writeFileSync } from "fs"; +import { readFileSync, writeFileSync } from "fs"; import * as esbuild from "esbuild"; import grabAllPages from "../../utils/grab-all-pages"; 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 grabClientHydrationScript from "./grab-client-hydration-script"; 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(); let build_starts = 0; const MAX_BUILD_STARTS = 10; @@ -18,9 +18,11 @@ export default async function allPagesBundler(params) { const dev = isDevelopment(); for (const page of pages) { const key = page.local_path; - const txt = grabClientHydrationScript({ + const txt = await grabClientHydrationScript({ page_local_path: page.local_path, }); + if (!txt) + continue; virtualEntries[key] = txt; } const virtualPlugin = { @@ -35,6 +37,19 @@ export default async function allPagesBundler(params) { loader: "tsx", 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 = { @@ -47,7 +62,6 @@ export default async function allPagesBundler(params) { 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) => { diff --git a/dist/functions/bundler/grab-client-hydration-script.d.ts b/dist/functions/bundler/grab-client-hydration-script.d.ts index 9e12275..321be36 100644 --- a/dist/functions/bundler/grab-client-hydration-script.d.ts +++ b/dist/functions/bundler/grab-client-hydration-script.d.ts @@ -1,5 +1,5 @@ type Params = { page_local_path: string; }; -export default function grabClientHydrationScript({ page_local_path }: Params): string; +export default function grabClientHydrationScript({ page_local_path, }: Params): Promise; export {}; diff --git a/dist/functions/bundler/grab-client-hydration-script.js b/dist/functions/bundler/grab-client-hydration-script.js index d196be1..9309c66 100644 --- a/dist/functions/bundler/grab-client-hydration-script.js +++ b/dist/functions/bundler/grab-client-hydration-script.js @@ -3,9 +3,11 @@ import path from "path"; import grabDirNames from "../../utils/grab-dir-names"; import AppNames from "../../utils/grab-app-names"; import grabConstants from "../../utils/grab-constants"; +import pagePathTransform from "../../utils/page-path-transform"; 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 target_path = pagePathTransform({ page_path: page_local_path }); const root_component_path = path.join(PAGES_DIR, `${AppNames["RootPagesComponentName"]}.tsx`); const does_root_exist = existsSync(root_component_path); let txt = ``; @@ -13,7 +15,7 @@ export default function grabClientHydrationScript({ page_local_path }) { if (does_root_exist) { 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`; if (does_root_exist) { txt += `const component = \n`; diff --git a/dist/functions/bundler/strip-server-side-logic.d.ts b/dist/functions/bundler/strip-server-side-logic.d.ts new file mode 100644 index 0000000..8398d8b --- /dev/null +++ b/dist/functions/bundler/strip-server-side-logic.d.ts @@ -0,0 +1,5 @@ +type Params = { + txt_code: string; +}; +export default function stripServerSideLogic({ txt_code }: Params): string; +export {}; diff --git a/dist/functions/bundler/strip-server-side-logic.js b/dist/functions/bundler/strip-server-side-logic.js new file mode 100644 index 0000000..97469d9 --- /dev/null +++ b/dist/functions/bundler/strip-server-side-logic.js @@ -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]); +} diff --git a/dist/functions/bunext-init.d.ts b/dist/functions/bunext-init.d.ts index 57b22e7..775fbd1 100644 --- a/dist/functions/bunext-init.d.ts +++ b/dist/functions/bunext-init.d.ts @@ -9,7 +9,7 @@ import { type FSWatcher } from "fs"; declare global { var ORA_SPINNER: Ora; var CONFIG: BunextConfig; - var SERVER: Server | undefined; + var SERVER: Server | undefined; var RECOMPILING: boolean; var WATCHER_TIMEOUT: any; var ROUTER: FileSystemRouter; diff --git a/dist/functions/server/start-server.d.ts b/dist/functions/server/start-server.d.ts index c3ef94e..d320c8a 100644 --- a/dist/functions/server/start-server.d.ts +++ b/dist/functions/server/start-server.d.ts @@ -1 +1 @@ -export default function startServer(): Promise; +export default function startServer(): Promise>; diff --git a/dist/functions/server/watcher.js b/dist/functions/server/watcher.js index b8f06c5..1d62149 100644 --- a/dist/functions/server/watcher.js +++ b/dist/functions/server/watcher.js @@ -3,6 +3,7 @@ import path from "path"; import grabDirNames from "../../utils/grab-dir-names"; import rebuildBundler from "./rebuild-bundler"; import { log } from "../../utils/log"; +import rewritePagesModule from "../../utils/rewrite-pages-module"; const { ROOT_DIR } = grabDirNames(); export default async function watcher() { await Bun.sleep(1000); @@ -36,6 +37,7 @@ export default async function watcher() { if (global.RECOMPILING) return; global.RECOMPILING = true; + await rewritePagesModule({ page_url: full_file_path }); await global.BUNDLER_CTX.rebuild(); } return; diff --git a/dist/functions/server/web-pages/grab-page-bundled-react-component.js b/dist/functions/server/web-pages/grab-page-bundled-react-component.js index cf7f8ad..612373c 100644 --- a/dist/functions/server/web-pages/grab-page-bundled-react-component.js +++ b/dist/functions/server/web-pages/grab-page-bundled-react-component.js @@ -1,25 +1,16 @@ 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"; export default async function grabPageBundledReactComponent({ file_path, root_file, server_res, }) { try { - let tsx = ``; - const server_res_json = JSON.stringify(EJSON.stringify(server_res || {}) ?? "{}"); - if (root_file) { - tsx += `import Root from "${root_file}"\n`; + let tsx = grabPageReactComponentString({ + file_path, + root_file, + 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 += ` \n`; - } - else { - tsx += ` \n`; - } - tsx += ` )\n`; - tsx += `}\n`; const mod = await grabTsxStringModule({ tsx, file_path }); const Main = mod.default; const component = _jsx(Main, {}); diff --git a/dist/functions/server/web-pages/grab-page-react-component-string.d.ts b/dist/functions/server/web-pages/grab-page-react-component-string.d.ts new file mode 100644 index 0000000..8f5efe8 --- /dev/null +++ b/dist/functions/server/web-pages/grab-page-react-component-string.d.ts @@ -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 {}; diff --git a/dist/functions/server/web-pages/grab-page-react-component-string.js b/dist/functions/server/web-pages/grab-page-react-component-string.js new file mode 100644 index 0000000..874f239 --- /dev/null +++ b/dist/functions/server/web-pages/grab-page-react-component-string.js @@ -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 += ` \n`; + } + else { + tsx += ` \n`; + } + tsx += ` )\n`; + tsx += `}\n`; + return tsx; + } + catch (error) { + return undefined; + } +} diff --git a/dist/types/index.d.ts b/dist/types/index.d.ts index 3e28009..1b2c567 100644 --- a/dist/types/index.d.ts +++ b/dist/types/index.d.ts @@ -1,5 +1,5 @@ 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 = { params: Record; searchParams: Record; @@ -71,7 +71,7 @@ export type BunxRouteParams = { * Intercept and Transform the response object */ resTransform?: (res: Response) => Promise | Response; - server?: Server; + server?: Server; }; export interface PostInsertReturn { fieldCount?: number; @@ -270,3 +270,4 @@ export type BunextCacheFileMeta = { paradigm: "html" | "json"; expiry_seconds?: number; }; +export type BunextRootComponentProps = PropsWithChildren & BunextPageProps; diff --git a/dist/utils/grab-dir-names.d.ts b/dist/utils/grab-dir-names.d.ts index f8d7673..09ac9df 100644 --- a/dist/utils/grab-dir-names.d.ts +++ b/dist/utils/grab-dir-names.d.ts @@ -5,6 +5,7 @@ export default function grabDirNames(): { API_DIR: string; PUBLIC_DIR: string; HYDRATION_DST_DIR: string; + BUNX_CWD_DIR: string; BUNX_ROOT_DIR: string; CONFIG_FILE: string; BUNX_TMP_DIR: string; @@ -18,4 +19,5 @@ export default function grabDirNames(): { HYDRATION_DST_DIR_MAP_JSON_FILE: string; BUNEXT_CACHE_DIR: string; BUNX_CWD_MODULE_CACHE_DIR: string; + BUNX_CWD_PAGES_REWRITE_DIR: string; }; diff --git a/dist/utils/grab-dir-names.js b/dist/utils/grab-dir-names.js index 37c354e..28cf1b6 100644 --- a/dist/utils/grab-dir-names.js +++ b/dist/utils/grab-dir-names.js @@ -12,6 +12,7 @@ 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_CWD_PAGES_REWRITE_DIR = path.resolve(BUNX_CWD_DIR, "pages"); 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_ROOT_DIR = path.resolve(__dirname, "../../"); @@ -28,6 +29,7 @@ export default function grabDirNames() { API_DIR, PUBLIC_DIR, HYDRATION_DST_DIR, + BUNX_CWD_DIR, BUNX_ROOT_DIR, CONFIG_FILE, BUNX_TMP_DIR, @@ -41,5 +43,6 @@ export default function grabDirNames() { HYDRATION_DST_DIR_MAP_JSON_FILE, BUNEXT_CACHE_DIR, BUNX_CWD_MODULE_CACHE_DIR, + BUNX_CWD_PAGES_REWRITE_DIR, }; } diff --git a/dist/utils/grab-router.d.ts b/dist/utils/grab-router.d.ts index 6d1730b..53c3bc8 100644 --- a/dist/utils/grab-router.d.ts +++ b/dist/utils/grab-router.d.ts @@ -1 +1 @@ -export default function grabRouter(): import("bun").FileSystemRouter; +export default function grabRouter(): Bun.FileSystemRouter; diff --git a/dist/utils/page-path-transform.d.ts b/dist/utils/page-path-transform.d.ts new file mode 100644 index 0000000..bcd5424 --- /dev/null +++ b/dist/utils/page-path-transform.d.ts @@ -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 {}; diff --git a/dist/utils/page-path-transform.js b/dist/utils/page-path-transform.js new file mode 100644 index 0000000..45632e5 --- /dev/null +++ b/dist/utils/page-path-transform.js @@ -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; +} diff --git a/dist/utils/rewrite-pages-module.d.ts b/dist/utils/rewrite-pages-module.d.ts new file mode 100644 index 0000000..4c76675 --- /dev/null +++ b/dist/utils/rewrite-pages-module.d.ts @@ -0,0 +1,5 @@ +type Params = { + page_url?: string | string[]; +}; +export default function rewritePagesModule(params?: Params): Promise; +export {}; diff --git a/dist/utils/rewrite-pages-module.js b/dist/utils/rewrite-pages-module.js new file mode 100644 index 0000000..a6a72cf --- /dev/null +++ b/dist/utils/rewrite-pages-module.js @@ -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, + }); + } +} diff --git a/package.json b/package.json index 46253bc..a553a60 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@moduletrace/bunext", "module": "index.ts", "type": "module", - "version": "1.0.9", + "version": "1.0.10", "main": "dist/index.js", "types": "dist/index.d.ts", "exports": { @@ -51,6 +51,10 @@ }, "dependencies": { "@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", "chalk": "^5.6.2", "commander": "^14.0.2", @@ -59,10 +63,6 @@ "lodash": "^4.17.23", "micromatch": "^4.0.8", "ora": "^9.0.0", - "postcss": "^8.5.8", - "@types/node": "^24.10.0", - "@types/bun": "latest", - "@types/react": "^19.2.2", - "@types/react-dom": "^19.2.2" + "postcss": "^8.5.8" } } diff --git a/src/commands/index.ts b/src/commands/index.ts index 9bbe793..2a54285 100755 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -5,6 +5,7 @@ import start from "./start"; import dev from "./dev"; import build from "./build"; import { log } from "../utils/log"; +import rewritePages from "./rewrite-pages"; /** * # Describe Program @@ -20,6 +21,7 @@ program program.addCommand(dev()); program.addCommand(start()); program.addCommand(build()); +program.addCommand(rewritePages()); /** * # Handle Unavailable Commands diff --git a/src/commands/rewrite-pages/index.ts b/src/commands/rewrite-pages/index.ts new file mode 100644 index 0000000..4060709 --- /dev/null +++ b/src/commands/rewrite-pages/index.ts @@ -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(); + }); +} diff --git a/src/functions/bundler/all-pages-bundler.ts b/src/functions/bundler/all-pages-bundler.ts index d0dd4ab..401bbba 100644 --- a/src/functions/bundler/all-pages-bundler.ts +++ b/src/functions/bundler/all-pages-bundler.ts @@ -1,4 +1,4 @@ -import { existsSync, statSync, writeFileSync } from "fs"; +import { readFileSync, writeFileSync } from "fs"; import * as esbuild from "esbuild"; import grabAllPages from "../../utils/grab-all-pages"; 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 grabClientHydrationScript from "./grab-client-hydration-script"; 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(); @@ -32,10 +32,12 @@ export default async function allPagesBundler(params?: Params) { for (const page of pages) { const key = page.local_path; - const txt = grabClientHydrationScript({ + const txt = await grabClientHydrationScript({ page_local_path: page.local_path, }); + if (!txt) continue; + virtualEntries[key] = txt; } @@ -52,6 +54,23 @@ export default async function allPagesBundler(params?: Params) { loader: "tsx", 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) { const error_msg = `Build Failed. Please check all your components and imports.`; log.error(error_msg); - // process.exit(1); } }); diff --git a/src/functions/bundler/grab-client-hydration-script.ts b/src/functions/bundler/grab-client-hydration-script.tsx similarity index 87% rename from src/functions/bundler/grab-client-hydration-script.ts rename to src/functions/bundler/grab-client-hydration-script.tsx index ba59199..124c0a2 100644 --- a/src/functions/bundler/grab-client-hydration-script.ts +++ b/src/functions/bundler/grab-client-hydration-script.tsx @@ -3,6 +3,7 @@ import path from "path"; import grabDirNames from "../../utils/grab-dir-names"; import AppNames from "../../utils/grab-app-names"; import grabConstants from "../../utils/grab-constants"; +import pagePathTransform from "../../utils/page-path-transform"; const { PAGES_DIR } = grabDirNames(); @@ -10,13 +11,17 @@ type Params = { page_local_path: string; }; -export default function grabClientHydrationScript({ page_local_path }: Params) { +export default async function grabClientHydrationScript({ + page_local_path, +}: Params) { 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`, @@ -30,7 +35,7 @@ export default function grabClientHydrationScript({ page_local_path }: Params) { if (does_root_exist) { 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`; if (does_root_exist) { diff --git a/src/functions/bundler/strip-server-side-logic.tsx b/src/functions/bundler/strip-server-side-logic.tsx new file mode 100644 index 0000000..6353271 --- /dev/null +++ b/src/functions/bundler/strip-server-side-logic.tsx @@ -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 = (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 = ( + 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]); +} diff --git a/src/functions/bunext-init.ts b/src/functions/bunext-init.ts index 915051e..b9b6a90 100644 --- a/src/functions/bunext-init.ts +++ b/src/functions/bunext-init.ts @@ -24,7 +24,7 @@ import cron from "./server/cron"; declare global { var ORA_SPINNER: Ora; var CONFIG: BunextConfig; - var SERVER: Server | undefined; + var SERVER: Server | undefined; var RECOMPILING: boolean; var WATCHER_TIMEOUT: any; var ROUTER: FileSystemRouter; diff --git a/src/functions/server/watcher.ts b/src/functions/server/watcher.ts index 5177db3..9d811e8 100644 --- a/src/functions/server/watcher.ts +++ b/src/functions/server/watcher.ts @@ -3,6 +3,7 @@ import path from "path"; import grabDirNames from "../../utils/grab-dir-names"; import rebuildBundler from "./rebuild-bundler"; import { log } from "../../utils/log"; +import rewritePagesModule from "../../utils/rewrite-pages-module"; const { ROOT_DIR } = grabDirNames(); @@ -47,6 +48,7 @@ export default async function watcher() { if (filename.match(target_files_match) && global.BUNDLER_CTX) { if (global.RECOMPILING) return; global.RECOMPILING = true; + await rewritePagesModule({ page_url: full_file_path }); await global.BUNDLER_CTX.rebuild(); } return; diff --git a/src/functions/server/web-pages/grab-page-bundled-react-component.tsx b/src/functions/server/web-pages/grab-page-bundled-react-component.tsx index 35d6517..9ed79fe 100644 --- a/src/functions/server/web-pages/grab-page-bundled-react-component.tsx +++ b/src/functions/server/web-pages/grab-page-bundled-react-component.tsx @@ -1,5 +1,5 @@ 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"; type Params = { @@ -14,28 +14,16 @@ export default async function grabPageBundledReactComponent({ server_res, }: Params): Promise { try { - let tsx = ``; + let tsx = grabPageReactComponentString({ + file_path, + root_file, + server_res, + }); - const server_res_json = JSON.stringify( - EJSON.stringify(server_res || {}) ?? "{}", - ); - - if (root_file) { - tsx += `import Root from "${root_file}"\n`; + 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 += ` \n`; - } else { - tsx += ` \n`; - } - tsx += ` )\n`; - tsx += `}\n`; - const mod = await grabTsxStringModule({ tsx, file_path }); const Main = mod.default; const component =
; diff --git a/src/functions/server/web-pages/grab-page-react-component-string.tsx b/src/functions/server/web-pages/grab-page-react-component-string.tsx new file mode 100644 index 0000000..be58587 --- /dev/null +++ b/src/functions/server/web-pages/grab-page-react-component-string.tsx @@ -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 += ` \n`; + } else { + tsx += ` \n`; + } + tsx += ` )\n`; + tsx += `}\n`; + + return tsx; + } catch (error: any) { + return undefined; + } +} diff --git a/src/types/index.ts b/src/types/index.ts index 3d42085..8919d77 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,5 +1,5 @@ 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 = { params: Record; @@ -84,7 +84,7 @@ export type BunxRouteParams = { * Intercept and Transform the response object */ resTransform?: (res: Response) => Promise | Response; - server?: Server; + server?: Server; }; export interface PostInsertReturn { @@ -293,3 +293,5 @@ export type BunextCacheFileMeta = { paradigm: "html" | "json"; expiry_seconds?: number; }; + +export type BunextRootComponentProps = PropsWithChildren & BunextPageProps; diff --git a/src/utils/grab-dir-names.ts b/src/utils/grab-dir-names.ts index 62130e0..a508bb2 100644 --- a/src/utils/grab-dir-names.ts +++ b/src/utils/grab-dir-names.ts @@ -20,6 +20,7 @@ export default function grabDirNames() { 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_HYDRATION_SRC_DIR = path.resolve( BUNX_CWD_DIR, @@ -49,6 +50,7 @@ export default function grabDirNames() { API_DIR, PUBLIC_DIR, HYDRATION_DST_DIR, + BUNX_CWD_DIR, BUNX_ROOT_DIR, CONFIG_FILE, BUNX_TMP_DIR, @@ -62,5 +64,6 @@ export default function grabDirNames() { HYDRATION_DST_DIR_MAP_JSON_FILE, BUNEXT_CACHE_DIR, BUNX_CWD_MODULE_CACHE_DIR, + BUNX_CWD_PAGES_REWRITE_DIR, }; } diff --git a/src/utils/page-path-transform.ts b/src/utils/page-path-transform.ts new file mode 100644 index 0000000..9f2d2a8 --- /dev/null +++ b/src/utils/page-path-transform.ts @@ -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; +} diff --git a/src/utils/rewrite-pages-module.ts b/src/utils/rewrite-pages-module.ts new file mode 100644 index 0000000..1eb187f --- /dev/null +++ b/src/utils/rewrite-pages-module.ts @@ -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, + }); + } +}