Bugfix and major refactor. Update server-side page rendering logic.
This commit is contained in:
parent
c2c63a1a99
commit
ef53ab5f2a
@ -6,6 +6,7 @@ export default async function () {
|
|||||||
const dirNames = grabDirNames();
|
const dirNames = grabDirNames();
|
||||||
|
|
||||||
execSync(`rm -rf ${dirNames.BUNEXT_CACHE_DIR}`);
|
execSync(`rm -rf ${dirNames.BUNEXT_CACHE_DIR}`);
|
||||||
|
execSync(`rm -rf ${dirNames.BUNX_CWD_MODULE_CACHE_DIR}`);
|
||||||
|
|
||||||
const keys = Object.keys(dirNames) as (keyof ReturnType<
|
const keys = Object.keys(dirNames) as (keyof ReturnType<
|
||||||
typeof grabDirNames
|
typeof grabDirNames
|
||||||
|
|||||||
63
src/functions/server/web-pages/grab-file-path-module.tsx
Normal file
63
src/functions/server/web-pages/grab-file-path-module.tsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import isDevelopment from "../../../utils/is-development";
|
||||||
|
import * as esbuild from "esbuild";
|
||||||
|
import postcss from "postcss";
|
||||||
|
import tailwindcss from "@tailwindcss/postcss";
|
||||||
|
import { readFile } from "fs/promises";
|
||||||
|
import grabDirNames from "../../../utils/grab-dir-names";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
type Params = {
|
||||||
|
file_path: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const tailwindPlugin: esbuild.Plugin = {
|
||||||
|
name: "tailwindcss",
|
||||||
|
setup(build) {
|
||||||
|
build.onLoad({ filter: /\.css$/ }, async (args) => {
|
||||||
|
const source = await readFile(args.path, "utf-8");
|
||||||
|
const result = await postcss([tailwindcss()]).process(source, {
|
||||||
|
from: args.path,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
contents: result.css,
|
||||||
|
loader: "css",
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function grabFilePathModule<T extends any = any>({
|
||||||
|
file_path,
|
||||||
|
}: Params): Promise<T> {
|
||||||
|
const dev = isDevelopment();
|
||||||
|
const { BUNX_CWD_MODULE_CACHE_DIR } = grabDirNames();
|
||||||
|
const target_cache_file_path = path.join(
|
||||||
|
BUNX_CWD_MODULE_CACHE_DIR,
|
||||||
|
`${path.basename(file_path)}.js`,
|
||||||
|
);
|
||||||
|
|
||||||
|
await esbuild.build({
|
||||||
|
entryPoints: [file_path],
|
||||||
|
bundle: true,
|
||||||
|
format: "esm",
|
||||||
|
target: "es2020",
|
||||||
|
platform: "node",
|
||||||
|
external: ["react", "react-dom"],
|
||||||
|
minify: true,
|
||||||
|
define: {
|
||||||
|
"process.env.NODE_ENV": JSON.stringify(
|
||||||
|
dev ? "development" : "production",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
metafile: true,
|
||||||
|
plugins: [tailwindPlugin],
|
||||||
|
jsx: "automatic",
|
||||||
|
outfile: target_cache_file_path,
|
||||||
|
});
|
||||||
|
|
||||||
|
Loader.registry.delete(target_cache_file_path);
|
||||||
|
const module = await import(`${target_cache_file_path}?t=${Date.now()}`);
|
||||||
|
|
||||||
|
return module as T;
|
||||||
|
}
|
||||||
@ -0,0 +1,51 @@
|
|||||||
|
import type { GrabPageReactBundledComponentRes } from "../../../types";
|
||||||
|
import EJSON from "../../../utils/ejson";
|
||||||
|
import grabTsxStringModule from "./grab-tsx-string-module";
|
||||||
|
|
||||||
|
type Params = {
|
||||||
|
file_path: string;
|
||||||
|
root_file?: string;
|
||||||
|
server_res?: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function grabPageBundledReactComponent({
|
||||||
|
file_path,
|
||||||
|
root_file,
|
||||||
|
server_res,
|
||||||
|
}: Params): Promise<GrabPageReactBundledComponentRes | undefined> {
|
||||||
|
try {
|
||||||
|
let tsx = ``;
|
||||||
|
|
||||||
|
const server_res_json = EJSON.stringify(server_res || {})?.replace(
|
||||||
|
/"/g,
|
||||||
|
'\\"',
|
||||||
|
);
|
||||||
|
|
||||||
|
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 {...props}><Page {...props} /></Root>\n`;
|
||||||
|
} else {
|
||||||
|
tsx += ` <Page {...props} />\n`;
|
||||||
|
}
|
||||||
|
tsx += ` )\n`;
|
||||||
|
tsx += `}\n`;
|
||||||
|
|
||||||
|
const mod = await grabTsxStringModule({ tsx, file_path });
|
||||||
|
const Main = mod.default;
|
||||||
|
const component = <Main />;
|
||||||
|
|
||||||
|
return {
|
||||||
|
component,
|
||||||
|
server_res,
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -11,6 +11,7 @@ import path from "path";
|
|||||||
import AppNames from "../../../utils/grab-app-names";
|
import AppNames from "../../../utils/grab-app-names";
|
||||||
import { existsSync } from "fs";
|
import { existsSync } from "fs";
|
||||||
import grabPageErrorComponent from "./grab-page-error-component";
|
import grabPageErrorComponent from "./grab-page-error-component";
|
||||||
|
import grabPageBundledReactComponent from "./grab-page-bundled-react-component";
|
||||||
|
|
||||||
class NotFoundError extends Error {}
|
class NotFoundError extends Error {}
|
||||||
|
|
||||||
@ -63,8 +64,6 @@ export default async function grabPageComponent({
|
|||||||
throw new Error(errMsg);
|
throw new Error(errMsg);
|
||||||
}
|
}
|
||||||
|
|
||||||
// const pageName = grabPageName({ path: file_path });
|
|
||||||
|
|
||||||
const root_pages_component_ts_file = `${path.join(PAGES_DIR, AppNames["RootPagesComponentName"])}.ts`;
|
const root_pages_component_ts_file = `${path.join(PAGES_DIR, AppNames["RootPagesComponentName"])}.ts`;
|
||||||
const root_pages_component_tsx_file = `${path.join(PAGES_DIR, AppNames["RootPagesComponentName"])}.tsx`;
|
const root_pages_component_tsx_file = `${path.join(PAGES_DIR, AppNames["RootPagesComponentName"])}.tsx`;
|
||||||
const root_pages_component_js_file = `${path.join(PAGES_DIR, AppNames["RootPagesComponentName"])}.js`;
|
const root_pages_component_js_file = `${path.join(PAGES_DIR, AppNames["RootPagesComponentName"])}.js`;
|
||||||
@ -82,17 +81,7 @@ export default async function grabPageComponent({
|
|||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
const root_module = root_file
|
const module: BunextPageModule = await import(file_path);
|
||||||
? await import(`${root_file}?t=${now}`)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const RootComponent = root_module?.default as FC<any> | undefined;
|
|
||||||
|
|
||||||
// const component_file_path = root_module
|
|
||||||
// ? `${file_path}`
|
|
||||||
// : `${file_path}?t=${global.LAST_BUILD_TIME ?? 0}`;
|
|
||||||
|
|
||||||
const module: BunextPageModule = await import(`${file_path}?t=${now}`);
|
|
||||||
|
|
||||||
const serverRes: BunextPageModuleServerReturn = await (async () => {
|
const serverRes: BunextPageModuleServerReturn = await (async () => {
|
||||||
try {
|
try {
|
||||||
@ -124,16 +113,18 @@ export default async function grabPageComponent({
|
|||||||
: undefined
|
: undefined
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const Component = module.default as FC<any>;
|
|
||||||
const Head = module.Head as FC<any>;
|
const Head = module.Head as FC<any>;
|
||||||
|
|
||||||
const component = RootComponent ? (
|
const { component } =
|
||||||
<RootComponent {...serverRes}>
|
(await grabPageBundledReactComponent({
|
||||||
<Component {...serverRes} />
|
file_path,
|
||||||
</RootComponent>
|
root_file,
|
||||||
) : (
|
server_res: serverRes,
|
||||||
<Component {...serverRes} />
|
})) || {};
|
||||||
);
|
|
||||||
|
if (!component) {
|
||||||
|
throw new Error(`Couldn't grab page component`);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
component,
|
component,
|
||||||
@ -152,3 +143,34 @@ export default async function grabPageComponent({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// let root_module: any;
|
||||||
|
|
||||||
|
// if (root_file) {
|
||||||
|
// if (isDevelopment()) {
|
||||||
|
// root_module = await grabFilePathModule({
|
||||||
|
// file_path: root_file,
|
||||||
|
// });
|
||||||
|
// } else {
|
||||||
|
// root_module = root_file ? await import(root_file) : undefined;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const RootComponent = root_module?.default as FC<any> | undefined;
|
||||||
|
|
||||||
|
// let module: BunextPageModule;
|
||||||
|
|
||||||
|
// if (isDevelopment()) {
|
||||||
|
// module = await grabFilePathModule({ file_path });
|
||||||
|
// } else {
|
||||||
|
// module = await import(file_path);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const Component = main_module.default as FC<any>;
|
||||||
|
// const component = RootComponent ? (
|
||||||
|
// <RootComponent {...serverRes}>
|
||||||
|
// <Component {...serverRes} />
|
||||||
|
// </RootComponent>
|
||||||
|
// ) : (
|
||||||
|
// <Component {...serverRes} />
|
||||||
|
// );
|
||||||
|
|||||||
76
src/functions/server/web-pages/grab-tsx-string-module.tsx
Normal file
76
src/functions/server/web-pages/grab-tsx-string-module.tsx
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import isDevelopment from "../../../utils/is-development";
|
||||||
|
import * as esbuild from "esbuild";
|
||||||
|
import postcss from "postcss";
|
||||||
|
import tailwindcss from "@tailwindcss/postcss";
|
||||||
|
import { readFile } from "fs/promises";
|
||||||
|
import grabDirNames from "../../../utils/grab-dir-names";
|
||||||
|
import path from "path";
|
||||||
|
import { execSync } from "child_process";
|
||||||
|
|
||||||
|
type Params = {
|
||||||
|
tsx: string;
|
||||||
|
file_path: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const tailwindPlugin: esbuild.Plugin = {
|
||||||
|
name: "tailwindcss",
|
||||||
|
setup(build) {
|
||||||
|
build.onLoad({ filter: /\.css$/ }, async (args) => {
|
||||||
|
const source = await readFile(args.path, "utf-8");
|
||||||
|
const result = await postcss([tailwindcss()]).process(source, {
|
||||||
|
from: args.path,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
contents: result.css,
|
||||||
|
loader: "css",
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function grabTsxStringModule<T extends any = any>({
|
||||||
|
tsx,
|
||||||
|
file_path,
|
||||||
|
}: Params): Promise<T> {
|
||||||
|
const dev = isDevelopment();
|
||||||
|
const { BUNX_CWD_MODULE_CACHE_DIR } = grabDirNames();
|
||||||
|
|
||||||
|
const trimmed_file_path = file_path
|
||||||
|
.replace(/.*\/src\/pages\//, "")
|
||||||
|
.replace(/\.tsx$/, "");
|
||||||
|
|
||||||
|
const out_file_path = path.join(
|
||||||
|
BUNX_CWD_MODULE_CACHE_DIR,
|
||||||
|
`${trimmed_file_path}.js`,
|
||||||
|
);
|
||||||
|
|
||||||
|
await esbuild.build({
|
||||||
|
stdin: {
|
||||||
|
contents: tsx,
|
||||||
|
resolveDir: process.cwd(),
|
||||||
|
loader: "tsx",
|
||||||
|
},
|
||||||
|
bundle: true,
|
||||||
|
format: "esm",
|
||||||
|
target: "es2020",
|
||||||
|
platform: "node",
|
||||||
|
external: ["react", "react-dom"],
|
||||||
|
minify: true,
|
||||||
|
define: {
|
||||||
|
"process.env.NODE_ENV": JSON.stringify(
|
||||||
|
dev ? "development" : "production",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
metafile: true,
|
||||||
|
plugins: [tailwindPlugin],
|
||||||
|
jsx: "automatic",
|
||||||
|
write: true,
|
||||||
|
outfile: out_file_path,
|
||||||
|
});
|
||||||
|
|
||||||
|
Loader.registry.delete(out_file_path);
|
||||||
|
const module = await import(`${out_file_path}?t=${Date.now()}`);
|
||||||
|
|
||||||
|
return module as T;
|
||||||
|
}
|
||||||
@ -17,7 +17,17 @@ export default function DefaultServerErrorPage({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<h1>500 Internal Server Error</h1>
|
<h1>500 Internal Server Error</h1>
|
||||||
<span>{children}</span>
|
<div
|
||||||
|
style={{
|
||||||
|
maxWidth: "800px",
|
||||||
|
overflowWrap: "break-word",
|
||||||
|
wordBreak: "break-all",
|
||||||
|
maxHeight: "80vh",
|
||||||
|
overflowY: "auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -243,6 +243,11 @@ export type GrabPageComponentRes = {
|
|||||||
head?: FC<BunextPageHeadFCProps>;
|
head?: FC<BunextPageHeadFCProps>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type GrabPageReactBundledComponentRes = {
|
||||||
|
component: JSX.Element;
|
||||||
|
server_res?: BunextPageModuleServerReturn;
|
||||||
|
};
|
||||||
|
|
||||||
export type PageFiles = {
|
export type PageFiles = {
|
||||||
local_path: string;
|
local_path: string;
|
||||||
url_path: string;
|
url_path: string;
|
||||||
|
|||||||
@ -16,6 +16,10 @@ 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_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,
|
||||||
@ -57,5 +61,6 @@ export default function grabDirNames() {
|
|||||||
BUNX_ROOT_404_FILE_NAME,
|
BUNX_ROOT_404_FILE_NAME,
|
||||||
HYDRATION_DST_DIR_MAP_JSON_FILE,
|
HYDRATION_DST_DIR_MAP_JSON_FILE,
|
||||||
BUNEXT_CACHE_DIR,
|
BUNEXT_CACHE_DIR,
|
||||||
|
BUNX_CWD_MODULE_CACHE_DIR,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
68
src/utils/register-dev-plugin.ts
Normal file
68
src/utils/register-dev-plugin.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import { resolve, dirname, extname } from "path";
|
||||||
|
import { existsSync } from "fs";
|
||||||
|
|
||||||
|
const SOURCE_EXTENSIONS = [".tsx", ".ts", ".jsx", ".js"];
|
||||||
|
|
||||||
|
function getLoader(filePath: string) {
|
||||||
|
const ext = extname(filePath).slice(1) as any;
|
||||||
|
return SOURCE_EXTENSIONS.map((e) => e.slice(1)).includes(ext) ? ext : "js";
|
||||||
|
}
|
||||||
|
|
||||||
|
function tryResolveSync(absPath: string): string | null {
|
||||||
|
if (existsSync(absPath)) return absPath;
|
||||||
|
for (const ext of SOURCE_EXTENSIONS) {
|
||||||
|
const p = absPath + ext;
|
||||||
|
if (existsSync(p)) return p;
|
||||||
|
}
|
||||||
|
for (const ext of SOURCE_EXTENSIONS) {
|
||||||
|
const p = resolve(absPath, "index" + ext);
|
||||||
|
if (existsSync(p)) return p;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function registerDevPlugin() {
|
||||||
|
Bun.plugin({
|
||||||
|
name: "bunext-dev-hmr",
|
||||||
|
setup(build) {
|
||||||
|
// Intercept absolute-path imports that already carry ?t= (our dynamic imports)
|
||||||
|
build.onResolve({ filter: /\?t=\d+$/ }, (args) => {
|
||||||
|
if (args.path.includes("node_modules")) return undefined;
|
||||||
|
const cleanPath = args.path.replace(/\?t=\d+$/, "");
|
||||||
|
const resolved = tryResolveSync(cleanPath);
|
||||||
|
if (!resolved) return undefined;
|
||||||
|
if (!SOURCE_EXTENSIONS.some((e) => resolved.endsWith(e)))
|
||||||
|
return undefined;
|
||||||
|
return {
|
||||||
|
path: `${resolved}?t=${global.LAST_BUILD_TIME ?? 0}`,
|
||||||
|
namespace: "bunext-dev",
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Intercept relative imports from within bunext-dev modules
|
||||||
|
build.onResolve({ filter: /^\./ }, (args) => {
|
||||||
|
if (!/\?t=\d+/.test(args.importer)) return undefined;
|
||||||
|
// Strip "namespace:" prefix (e.g. "bunext-dev:") Bun prepends to importer
|
||||||
|
const cleanImporter = args.importer
|
||||||
|
.replace(/^[^/]+:(?=\/)/, "")
|
||||||
|
.replace(/\?t=\d+$/, "");
|
||||||
|
const base = resolve(dirname(cleanImporter), args.path);
|
||||||
|
const resolved = tryResolveSync(base);
|
||||||
|
if (!resolved) return undefined;
|
||||||
|
if (!SOURCE_EXTENSIONS.some((e) => resolved.endsWith(e)))
|
||||||
|
return undefined;
|
||||||
|
return {
|
||||||
|
path: `${resolved}?t=${global.LAST_BUILD_TIME ?? 0}`,
|
||||||
|
namespace: "bunext-dev",
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load files in the bunext-dev namespace from disk (async is fine in onLoad)
|
||||||
|
build.onLoad({ filter: /.*/, namespace: "bunext-dev" }, async (args) => {
|
||||||
|
const realPath = args.path.replace(/\?t=\d+$/, "");
|
||||||
|
const source = await Bun.file(realPath).text();
|
||||||
|
return { contents: source, loader: getLoader(realPath) };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user