Major Bugfix. Fix server component client compatibility

This commit is contained in:
2026-03-22 10:34:30 +01:00
parent 9f619c8898
commit a9af20a8b2
39 changed files with 508 additions and 79 deletions
+18 -4
View File
@@ -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) => {
+1 -1
View File
@@ -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<string>;
export {};
+4 -2
View File
@@ -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 = <Root suppressHydrationWarning={true} {...pageProps}><Page {...pageProps} /></Root>\n`;
+5
View File
@@ -0,0 +1,5 @@
type Params = {
txt_code: string;
};
export default function stripServerSideLogic({ txt_code }: Params): string;
export {};
+61
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]);
}
+1 -1
View File
@@ -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<any> | undefined;
var RECOMPILING: boolean;
var WATCHER_TIMEOUT: any;
var ROUTER: FileSystemRouter;
+1 -1
View File
@@ -1 +1 @@
export default function startServer(): Promise<import("bun").Server>;
export default function startServer(): Promise<Bun.Server<undefined>>;
+2
View File
@@ -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;
@@ -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 += ` <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 Main = mod.default;
const component = _jsx(Main, {});
@@ -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 {};
@@ -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;
}
}