Major Bugfix. Fix server component client compatibility
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
+7
-2
@@ -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) {
|
||||
@@ -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]);
|
||||
}
|
||||
@@ -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<any> | undefined;
|
||||
var RECOMPILING: boolean;
|
||||
var WATCHER_TIMEOUT: any;
|
||||
var ROUTER: FileSystemRouter;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<GrabPageReactBundledComponentRes | undefined> {
|
||||
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 += ` <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 = <Main />;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
+4
-2
@@ -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<string, string>;
|
||||
@@ -84,7 +84,7 @@ export type BunxRouteParams = {
|
||||
* Intercept and Transform the response object
|
||||
*/
|
||||
resTransform?: (res: Response) => Promise<Response> | Response;
|
||||
server?: Server;
|
||||
server?: Server<any>;
|
||||
};
|
||||
|
||||
export interface PostInsertReturn {
|
||||
@@ -293,3 +293,5 @@ export type BunextCacheFileMeta = {
|
||||
paradigm: "html" | "json";
|
||||
expiry_seconds?: number;
|
||||
};
|
||||
|
||||
export type BunextRootComponentProps = PropsWithChildren & BunextPageProps;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user