Update HMR. Make it true HMR. Add URL to page server props

This commit is contained in:
Benjamin Toby 2026-03-20 11:19:22 +01:00
parent 7804a34951
commit 52dde6c0ab
52 changed files with 1548 additions and 708 deletions

View File

@ -9,6 +9,7 @@ export default function () {
.action(async () => {
log.banner();
log.info("Starting production server ...");
process.env.NODE_ENV = "production";
await init();
const config = await grabConfig();
global.CONFIG = { ...config };

View File

@ -2,4 +2,6 @@ export const AppData = {
DefaultCacheExpiryTimeSeconds: 60 * 60,
DefaultCronInterval: 30000,
BunextStaticFilesCacheExpiry: 60 * 60 * 24 * 7,
ClientHMRPath: "__bunext_client_hmr__",
BunextClientHydrationScriptID: "bunext-client-hydration-script",
};

View File

@ -1,56 +1,23 @@
import { existsSync, writeFileSync } from "fs";
import path from "path";
import { writeFileSync } from "fs";
import * as esbuild from "esbuild";
import postcss from "postcss";
import tailwindcss from "@tailwindcss/postcss";
import { readFile } from "fs/promises";
import grabAllPages from "../../utils/grab-all-pages";
import grabDirNames from "../../utils/grab-dir-names";
import AppNames from "../../utils/grab-app-names";
import isDevelopment from "../../utils/is-development";
import { execSync } from "child_process";
import grabConstants from "../../utils/grab-constants";
import { log } from "../../utils/log";
const { HYDRATION_DST_DIR, PAGES_DIR, HYDRATION_DST_DIR_MAP_JSON_FILE } = grabDirNames();
const tailwindPlugin = {
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",
};
});
},
};
import tailwindEsbuildPlugin from "../server/web-pages/tailwind-esbuild-plugin";
import grabClientHydrationScript from "./grab-client-hydration-script";
import grabArtifactsFromBundledResults from "./grab-artifacts-from-bundled-result";
const { HYDRATION_DST_DIR, HYDRATION_DST_DIR_MAP_JSON_FILE } = grabDirNames();
export default async function allPagesBundler(params) {
const pages = grabAllPages({ exclude_api: true });
const { ClientRootElementIDName, ClientRootComponentWindowName } = grabConstants();
const virtualEntries = {};
const dev = isDevelopment();
const root_component_path = path.join(PAGES_DIR, `${AppNames["RootPagesComponentName"]}.tsx`);
const does_root_exist = existsSync(root_component_path);
for (const page of pages) {
const key = page.local_path;
let txt = ``;
txt += `import { hydrateRoot } from "react-dom/client";\n`;
if (does_root_exist) {
txt += `import Root from "${root_component_path}";\n`;
}
txt += `import Page from "${page.local_path}";\n\n`;
txt += `const pageProps = window.__PAGE_PROPS__ || {};\n`;
if (does_root_exist) {
txt += `const component = <Root {...pageProps}><Page {...pageProps} /></Root>\n`;
}
else {
txt += `const component = <Page {...pageProps} />\n`;
}
txt += `const root = hydrateRoot(document.getElementById("${ClientRootElementIDName}"), component);\n\n`;
txt += `window.${ClientRootComponentWindowName} = root;\n`;
const txt = grabClientHydrationScript({
page_local_path: page.local_path,
});
virtualEntries[key] = txt;
}
const virtualPlugin = {
@ -77,38 +44,18 @@ export default async function allPagesBundler(params) {
build.onEnd((result) => {
if (result.errors.length > 0)
return;
const artifacts = Object.entries(result.metafile.outputs)
.filter(([, meta]) => meta.entryPoint)
.map(([outputPath, meta]) => {
const target_page = pages.find((p) => {
return (meta.entryPoint === `virtual:${p.local_path}`);
});
if (!target_page || !meta.entryPoint) {
return undefined;
}
const { file_name, local_path, url_path } = target_page;
const cssPath = meta.cssBundle || undefined;
return {
path: outputPath,
hash: path.basename(outputPath, path.extname(outputPath)),
type: outputPath.endsWith(".css")
? "text/css"
: "text/javascript",
entrypoint: meta.entryPoint,
css_path: cssPath,
file_name,
local_path,
url_path,
};
const artifacts = grabArtifactsFromBundledResults({
pages,
result,
});
if (artifacts.length > 0) {
const final_artifacts = artifacts.filter((a) => Boolean(a?.entrypoint));
global.BUNDLER_CTX_MAP = final_artifacts;
params?.post_build_fn?.({ artifacts: final_artifacts });
if (artifacts?.[0] && artifacts.length > 0) {
global.BUNDLER_CTX_MAP = artifacts;
global.PAGE_FILES = pages;
params?.post_build_fn?.({ artifacts });
writeFileSync(HYDRATION_DST_DIR_MAP_JSON_FILE, JSON.stringify(artifacts));
}
const elapsed = (performance.now() - buildStart).toFixed(0);
log.success(`Built in ${elapsed}ms`);
log.success(`[Built] in ${elapsed}ms`);
if (params?.exit_after_first_build) {
process.exit();
}
@ -129,9 +76,10 @@ export default async function allPagesBundler(params) {
},
entryNames: "[dir]/[name]/[hash]",
metafile: true,
plugins: [tailwindPlugin, virtualPlugin, artifactTracker],
plugins: [tailwindEsbuildPlugin, virtualPlugin, artifactTracker],
jsx: "automatic",
splitting: true,
logLevel: "silent",
});
await ctx.rebuild();
if (params?.watch) {

View File

@ -0,0 +1,35 @@
import path from "path";
import * as esbuild from "esbuild";
export default function grabArtifactsFromBundledResults({ result, pages, }) {
if (result.errors.length > 0)
return;
const artifacts = Object.entries(result.metafile.outputs)
.filter(([, meta]) => meta.entryPoint)
.map(([outputPath, meta]) => {
const target_page = pages.find((p) => {
return meta.entryPoint === `virtual:${p.local_path}`;
});
if (!target_page || !meta.entryPoint) {
return undefined;
}
const { file_name, local_path, url_path } = target_page;
const cssPath = meta.cssBundle || undefined;
return {
path: outputPath,
hash: path.basename(outputPath, path.extname(outputPath)),
type: outputPath.endsWith(".css")
? "text/css"
: "text/javascript",
entrypoint: meta.entryPoint,
css_path: cssPath,
file_name,
local_path,
url_path,
};
});
if (artifacts.length > 0) {
const final_artifacts = artifacts.filter((a) => Boolean(a?.entrypoint));
return final_artifacts;
}
return undefined;
}

View File

@ -0,0 +1,65 @@
import { existsSync } from "fs";
import path from "path";
import grabDirNames from "../../utils/grab-dir-names";
import AppNames from "../../utils/grab-app-names";
import grabConstants from "../../utils/grab-constants";
const { PAGES_DIR } = grabDirNames();
export default function grabClientHydrationScript({ page_local_path }) {
const { ClientRootElementIDName, ClientRootComponentWindowName } = grabConstants();
const root_component_path = path.join(PAGES_DIR, `${AppNames["RootPagesComponentName"]}.tsx`);
const does_root_exist = existsSync(root_component_path);
// let txt = ``;
// txt += `import { hydrateRoot } from "react-dom/client";\n`;
// if (does_root_exist) {
// txt += `import Root from "${root_component_path}";\n`;
// }
// txt += `import Page from "${page.local_path}";\n\n`;
// txt += `const pageProps = window.__PAGE_PROPS__ || {};\n`;
// if (does_root_exist) {
// txt += `const component = <Root {...pageProps}><Page {...pageProps} /></Root>\n`;
// } else {
// txt += `const component = <Page {...pageProps} />\n`;
// }
// txt += `const root = hydrateRoot(document.getElementById("${ClientRootElementIDName}"), component);\n\n`;
// txt += `window.${ClientRootComponentWindowName} = root;\n`;
let txt = ``;
// txt += `import * as React from "react";\n`;
// txt += `import * as ReactDOM from "react-dom";\n`;
// txt += `import * as ReactDOMClient from "react-dom/client";\n`;
// txt += `import * as JSXRuntime from "react/jsx-runtime";\n`;
txt += `import { hydrateRoot, createElement } from "react-dom/client";\n`;
if (does_root_exist) {
txt += `import Root from "${root_component_path}";\n`;
}
txt += `import Page from "${page_local_path}";\n\n`;
// txt += `window.__REACT__ = React;\n`;
// txt += `window.__REACT_DOM__ = ReactDOM;\n`;
// txt += `window.__REACT_DOM_CLIENT__ = ReactDOMClient;\n`;
// txt += `window.__JSX_RUNTIME__ = JSXRuntime;\n\n`;
txt += `const pageProps = window.__PAGE_PROPS__ || {};\n`;
if (does_root_exist) {
txt += `const component = <Root {...pageProps}><Page {...pageProps} /></Root>\n`;
}
else {
txt += `const component = <Page {...pageProps} />\n`;
}
txt += `if (window.${ClientRootComponentWindowName}?.render) {\n`;
txt += ` window.${ClientRootComponentWindowName}.render(component);\n`;
txt += `} else {\n`;
txt += ` const root = hydrateRoot(document.getElementById("${ClientRootElementIDName}"), component);\n\n`;
txt += ` window.${ClientRootComponentWindowName} = root;\n`;
txt += ` window.__BUNEXT_RERENDER__ = (NewPage) => {\n`;
txt += ` const props = window.__PAGE_PROPS__ || {};\n`;
txt += ` root.render(<NewPage {...props} />);\n`;
txt += ` };\n`;
txt += `}\n`;
// // HMR re-render helper
// if (does_root_exist) {
// txt += `window.__BUNEXT_RERENDER__ = (NewPage) => {\n`;
// txt += ` const props = window.__PAGE_PROPS__ || {};\n`;
// txt += ` root.render(<Root {...props}><NewPage {...props} /></Root>);\n`;
// txt += `};\n`;
// } else {
// }
return txt;
}

View File

@ -1,10 +1,16 @@
import { existsSync, mkdirSync, statSync, writeFileSync } from "fs";
import grabDirNames from "../utils/grab-dir-names";
import { execSync } from "child_process";
import path from "path";
export default async function () {
const dirNames = grabDirNames();
execSync(`rm -rf ${dirNames.BUNEXT_CACHE_DIR}`);
execSync(`rm -rf ${dirNames.BUNX_CWD_MODULE_CACHE_DIR}`);
try {
const current_version = (await Bun.file(path.resolve(__dirname, "../../package.json")).json()).version;
global.CURRENT_VERSION = current_version;
}
catch (error) { }
const keys = Object.keys(dirNames);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];

24
dist/functions/server/handle-files.js vendored Normal file
View File

@ -0,0 +1,24 @@
import grabDirNames from "../../utils/grab-dir-names";
import path from "path";
import isDevelopment from "../../utils/is-development";
import { existsSync } from "fs";
const { PUBLIC_DIR } = grabDirNames();
export default async function ({ req, server }) {
try {
const is_dev = isDevelopment();
const url = new URL(req.url);
const file_path = path.join(PUBLIC_DIR, url.pathname);
if (!existsSync(file_path)) {
return new Response(`File Doesn't Exist`, {
status: 404,
});
}
const file = Bun.file(file_path);
return new Response(file);
}
catch (error) {
return new Response(`File Not Found`, {
status: 404,
});
}
}

View File

@ -0,0 +1,54 @@
import grabDirNames from "../../utils/grab-dir-names";
import { AppData } from "../../data/app-data";
import path from "path";
import grabRootFile from "./web-pages/grab-root-file";
import grabPageBundledReactComponent from "./web-pages/grab-page-bundled-react-component";
import writeHMRTsxModule from "./web-pages/write-hmr-tsx-module";
const { PUBLIC_DIR, BUNX_HYDRATION_SRC_DIR } = grabDirNames();
export default async function ({ req, server }) {
try {
const url = new URL(req.url);
const target_href = url.searchParams.get("href");
if (!target_href) {
return new Response(`No HREF passed to /${AppData["ClientHMRPath"]}`, { status: 404 });
}
const target_href_url = new URL(target_href);
const match = global.ROUTER.match(target_href_url.pathname);
if (!match?.filePath) {
return new Response(`No pages file matched for this path`, {
status: 404,
});
}
const out_file = path.join(BUNX_HYDRATION_SRC_DIR, target_href_url.pathname, "index.js");
const { root_file } = grabRootFile();
const { tsx } = (await grabPageBundledReactComponent({
file_path: match.filePath,
root_file,
})) || {};
if (!tsx) {
throw new Error(`Couldn't grab txt string`);
}
const artifact = await writeHMRTsxModule({
tsx,
out_file,
});
const file = Bun.file(out_file);
if (await file.exists()) {
return new Response(file, {
headers: {
"Content-Type": "text/javascript",
},
});
}
return new Response("Not found", {
status: 404,
});
}
catch (error) {
const error_msg = error.message;
console.error(error_msg);
return new Response(error_msg || "HMR Error", {
status: 404,
});
}
}

34
dist/functions/server/handle-hmr.js vendored Normal file
View File

@ -0,0 +1,34 @@
import grabRouteParams from "../../utils/grab-route-params";
import grabConstants from "../../utils/grab-constants";
import grabRouter from "../../utils/grab-router";
export default async function ({ req, server }) {
const referer_url = new URL(req.headers.get("referer") || "");
const match = global.ROUTER.match(referer_url.pathname);
const target_map = match?.filePath
? global.BUNDLER_CTX_MAP?.find((m) => m.local_path == match.filePath)
: undefined;
let controller;
const stream = new ReadableStream({
start(c) {
controller = c;
global.HMR_CONTROLLERS.push({
controller: c,
page_url: referer_url.href,
target_map,
});
},
cancel() {
const targetControllerIndex = global.HMR_CONTROLLERS.findIndex((c) => c.controller == controller);
if (typeof targetControllerIndex == "number" &&
targetControllerIndex >= 0) {
global.HMR_CONTROLLERS.splice(targetControllerIndex, 1);
}
},
});
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
Connection: "keep-alive",
},
});
}

25
dist/functions/server/handle-public.js vendored Normal file
View File

@ -0,0 +1,25 @@
import grabDirNames from "../../utils/grab-dir-names";
import path from "path";
import isDevelopment from "../../utils/is-development";
import { existsSync } from "fs";
const { PUBLIC_DIR, BUNX_HYDRATION_SRC_DIR } = grabDirNames();
export default async function ({ req, server }) {
try {
const is_dev = isDevelopment();
const url = new URL(req.url);
const file_path = path.join(PUBLIC_DIR, url.pathname.replace(/^\/public/, ""));
if (!existsSync(file_path)) {
return new Response(`Public File Doesn't Exist`, {
status: 404,
});
}
const file = Bun.file(file_path);
let res_opts = {};
return new Response(file, res_opts);
}
catch (error) {
return new Response(`Public File Not Found`, {
status: 404,
});
}
}

View File

@ -1,21 +1,22 @@
import path from "path";
import grabAppPort from "../../utils/grab-app-port";
import grabDirNames from "../../utils/grab-dir-names";
import handleWebPages from "./web-pages/handle-web-pages";
import handleRoutes from "./handle-routes";
import isDevelopment from "../../utils/is-development";
import grabConstants from "../../utils/grab-constants";
import { AppData } from "../../data/app-data";
import { existsSync } from "fs";
import handleHmr from "./handle-hmr";
import handleHmrUpdate from "./handle-hmr-update";
import handlePublic from "./handle-public";
import handleFiles from "./handle-files";
export default async function (params) {
const port = grabAppPort();
const { PUBLIC_DIR } = grabDirNames();
const is_dev = isDevelopment();
return {
async fetch(req, server) {
try {
const url = new URL(req.url);
const { config } = grabConstants();
let response = undefined;
if (config?.middleware) {
const middleware_res = await config.middleware({
req,
@ -26,81 +27,31 @@ export default async function (params) {
return middleware_res;
}
}
if (url.pathname === "/__hmr" && is_dev) {
const referer_url = new URL(req.headers.get("referer") || "");
const match = global.ROUTER.match(referer_url.pathname);
const target_map = match?.filePath
? global.BUNDLER_CTX_MAP?.find((m) => m.local_path == match.filePath)
: undefined;
let controller;
const stream = new ReadableStream({
start(c) {
controller = c;
global.HMR_CONTROLLERS.push({
controller: c,
page_url: referer_url.href,
target_map,
});
},
cancel() {
const targetControllerIndex = global.HMR_CONTROLLERS.findIndex((c) => c.controller == controller);
if (typeof targetControllerIndex == "number" &&
targetControllerIndex >= 0) {
global.HMR_CONTROLLERS.splice(targetControllerIndex, 1);
}
},
});
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
});
if (url.pathname == `/${AppData["ClientHMRPath"]}`) {
response = await handleHmrUpdate({ req, server });
}
if (url.pathname.startsWith("/api/")) {
return await handleRoutes({ req, server });
else if (url.pathname === "/__hmr" && is_dev) {
response = await handleHmr({ req, server });
}
if (url.pathname.startsWith("/public/")) {
try {
const file_path = path.join(PUBLIC_DIR, url.pathname.replace(/^\/public/, ""));
if (!existsSync(file_path)) {
return new Response(`Public File Doesn't Exist`, {
status: 404,
});
}
const file = Bun.file(file_path);
let res_opts = {};
if (!is_dev && url.pathname.match(/__bunext/)) {
res_opts.headers = {
"Cache-Control": `public, max-age=${AppData["BunextStaticFilesCacheExpiry"]}, must-revalidate`,
};
}
return new Response(file, res_opts);
}
catch (error) {
return new Response(`Public File Not Found`, {
status: 404,
});
}
else if (url.pathname.startsWith("/api/")) {
response = await handleRoutes({ req, server });
}
// if (url.pathname.startsWith("/favicon.") ) {
if (url.pathname.match(/\..*$/)) {
try {
const file_path = path.join(PUBLIC_DIR, url.pathname);
if (!existsSync(file_path)) {
return new Response(`File Doesn't Exist`, {
status: 404,
});
}
const file = Bun.file(file_path);
return new Response(file);
}
catch (error) {
return new Response(`File Not Found`, { status: 404 });
}
else if (url.pathname.startsWith("/public/")) {
response = await handlePublic({ req, server });
}
return await handleWebPages({ req });
else if (url.pathname.match(/\..*$/)) {
response = await handleFiles({ req, server });
}
else {
response = await handleWebPages({ req });
}
if (!response) {
throw new Error(`No Response generated`);
}
if (is_dev) {
response.headers.set("Cache-Control", "no-cache, no-store, must-revalidate");
}
return response;
}
catch (error) {
return new Response(`Server Error: ${error.message}`, {

View File

@ -5,7 +5,7 @@ import rebuildBundler from "./rebuild-bundler";
import { log } from "../../utils/log";
const { SRC_DIR } = grabDirNames();
export default function watcher() {
watch(SRC_DIR, {
const pages_src_watcher = watch(SRC_DIR, {
recursive: true,
persistent: true,
}, async (event, filename) => {
@ -13,6 +13,8 @@ export default function watcher() {
return;
if (event !== "rename")
return;
if (!filename.match(/^pages\//))
return;
if (global.RECOMPILING)
return;
const fullPath = path.join(SRC_DIR, filename);
@ -28,5 +30,10 @@ export default function watcher() {
finally {
global.RECOMPILING = false;
}
if (global.PAGES_SRC_WATCHER) {
global.PAGES_SRC_WATCHER.close();
watcher();
}
});
global.PAGES_SRC_WATCHER = pages_src_watcher;
}

View File

@ -5,10 +5,18 @@ import EJSON from "../../../utils/ejson";
import isDevelopment from "../../../utils/is-development";
import grabWebPageHydrationScript from "./grab-web-page-hydration-script";
import grabWebMetaHTML from "./grab-web-meta-html";
export default async function genWebHTML({ component, pageProps, bundledMap, head: Head, module, meta, routeParams, }) {
import { log } from "../../../utils/log";
import { AppData } from "../../../data/app-data";
export default async function genWebHTML({ component, pageProps, bundledMap, head: Head, module, meta, routeParams, debug, }) {
const { ClientRootElementIDName, ClientWindowPagePropsName } = grabContants();
const { renderToString } = await import(path.join(process.cwd(), "node_modules", "react-dom", "server"));
if (debug) {
log.info("component", component);
}
const componentHTML = renderToString(component);
if (debug) {
log.info("componentHTML", componentHTML);
}
const headHTML = Head
? renderToString(_jsx(Head, { serverRes: pageProps, ctx: routeParams }))
: "";
@ -25,7 +33,7 @@ export default async function genWebHTML({ component, pageProps, bundledMap, hea
}
html += ` <script>window.${ClientWindowPagePropsName} = ${EJSON.stringify(pageProps || {}) || "{}"}</script>\n`;
if (bundledMap?.path) {
html += ` <script src="/${bundledMap.path}" type="module" async></script>\n`;
html += ` <script src="/${bundledMap.path}" type="module" id="${AppData["BunextClientHydrationScriptID"]}" async></script>\n`;
}
if (isDevelopment()) {
html += `<script defer>\n${await grabWebPageHydrationScript({ bundledMap })}\n</script>\n`;

View File

@ -0,0 +1,55 @@
import isDevelopment from "../../../utils/is-development";
import { log } from "../../../utils/log";
import writeCache from "../../cache/write-cache";
import genWebHTML from "./generate-web-html";
export default async function generateWebPageResponseFromComponentReturn({ component, module, bundledMap, head, meta, routeParams, serverRes, debug, }) {
const html = await genWebHTML({
component,
pageProps: serverRes,
bundledMap,
module,
meta,
head,
routeParams,
debug,
});
if (debug) {
log.info("html", html);
}
if (serverRes?.redirect?.destination) {
return Response.redirect(serverRes.redirect.destination, serverRes.redirect.permanent
? 301
: serverRes.redirect.status_code || 302);
}
const res_opts = {
...serverRes?.responseOptions,
headers: {
"Content-Type": "text/html",
...serverRes?.responseOptions?.headers,
},
};
if (isDevelopment()) {
res_opts.headers = {
...res_opts.headers,
"Cache-Control": "no-cache, no-store, must-revalidate",
Pragma: "no-cache",
Expires: "0",
};
}
const cache_page = module.config?.cachePage || serverRes?.cachePage || false;
const expiry_seconds = module.config?.cacheExpiry || serverRes?.cacheExpiry;
if (cache_page && routeParams?.url) {
const key = routeParams.url.pathname + (routeParams.url.search || "");
writeCache({
key,
value: html,
paradigm: "html",
expiry_seconds,
});
}
const res = new Response(html, res_opts);
if (routeParams?.resTransform) {
return await routeParams.resTransform(res);
}
return res;
}

View File

@ -5,25 +5,12 @@ import tailwindcss from "@tailwindcss/postcss";
import { readFile } from "fs/promises";
import grabDirNames from "../../../utils/grab-dir-names";
import path from "path";
const tailwindPlugin = {
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({ file_path, }) {
import tailwindEsbuildPlugin from "./tailwind-esbuild-plugin";
export default async function grabFilePathModule({ file_path, out_file, }) {
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`);
const target_cache_file_path = out_file ||
path.join(BUNX_CWD_MODULE_CACHE_DIR, `${path.basename(file_path)}.js`);
await esbuild.build({
entryPoints: [file_path],
bundle: true,
@ -36,7 +23,7 @@ export default async function grabFilePathModule({ file_path, }) {
"process.env.NODE_ENV": JSON.stringify(dev ? "development" : "production"),
},
metafile: true,
plugins: [tailwindPlugin],
plugins: [tailwindEsbuildPlugin],
jsx: "automatic",
outfile: target_cache_file_path,
});

View File

@ -13,10 +13,10 @@ export default async function grabPageBundledReactComponent({ file_path, root_fi
tsx += `const props = JSON.parse("${server_res_json}")\n\n`;
tsx += ` return (\n`;
if (root_file) {
tsx += ` <Root {...props}><Page {...props} /></Root>\n`;
tsx += ` <Root suppressHydrationWarning={true} {...props}><Page {...props} /></Root>\n`;
}
else {
tsx += ` <Page {...props} />\n`;
tsx += ` <Page suppressHydrationWarning={true} {...props} />\n`;
}
tsx += ` )\n`;
tsx += `}\n`;
@ -26,6 +26,7 @@ export default async function grabPageBundledReactComponent({ file_path, root_fi
return {
component,
server_res,
tsx,
};
}
catch (error) {

View File

@ -1,17 +1,14 @@
import grabDirNames from "../../../utils/grab-dir-names";
import grabRouteParams from "../../../utils/grab-route-params";
import path from "path";
import AppNames from "../../../utils/grab-app-names";
import { existsSync } from "fs";
import grabPageErrorComponent from "./grab-page-error-component";
import grabPageBundledReactComponent from "./grab-page-bundled-react-component";
import _ from "lodash";
import { log } from "../../../utils/log";
import grabRootFile from "./grab-root-file";
class NotFoundError extends Error {
}
export default async function grabPageComponent({ req, file_path: passed_file_path, }) {
export default async function grabPageComponent({ req, file_path: passed_file_path, debug, }) {
const url = req?.url ? new URL(req.url) : undefined;
const router = global.ROUTER;
const { PAGES_DIR } = grabDirNames();
let routeParams = undefined;
try {
routeParams = req ? await grabRouteParams({ req }) : undefined;
@ -19,11 +16,17 @@ export default async function grabPageComponent({ req, file_path: passed_file_pa
if (url_path && url?.search) {
url_path += url.search;
}
if (debug) {
log.info(`url_path:`, url_path);
}
const match = url_path ? router.match(url_path) : undefined;
if (!match?.filePath && url?.pathname) {
throw new NotFoundError(`Page ${url.pathname} not found`);
}
const file_path = match?.filePath || passed_file_path;
if (debug) {
log.info(`file_path:`, file_path);
}
if (!file_path) {
const errMsg = `No File Path (\`file_path\`) or Request Object (\`req\`) provided not found`;
// console.error(errMsg);
@ -35,21 +38,14 @@ export default async function grabPageComponent({ req, file_path: passed_file_pa
console.error(errMsg);
throw new Error(errMsg);
}
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_js_file = `${path.join(PAGES_DIR, AppNames["RootPagesComponentName"])}.js`;
const root_pages_component_jsx_file = `${path.join(PAGES_DIR, AppNames["RootPagesComponentName"])}.jsx`;
const root_file = existsSync(root_pages_component_tsx_file)
? root_pages_component_tsx_file
: existsSync(root_pages_component_ts_file)
? root_pages_component_ts_file
: existsSync(root_pages_component_jsx_file)
? root_pages_component_jsx_file
: existsSync(root_pages_component_js_file)
? root_pages_component_js_file
: undefined;
const now = Date.now();
if (debug) {
log.info(`bundledMap:`, bundledMap);
}
const { root_file } = grabRootFile();
const module = await import(file_path);
if (debug) {
log.info(`module:`, module);
}
const serverRes = await (async () => {
const default_props = {
url: {
@ -88,6 +84,9 @@ export default async function grabPageComponent({ req, file_path: passed_file_pa
};
}
})();
if (debug) {
log.info(`serverRes:`, serverRes);
}
const meta = module.meta
? typeof module.meta == "function" && routeParams
? await module.meta({
@ -98,6 +97,9 @@ export default async function grabPageComponent({ req, file_path: passed_file_pa
? module.meta
: undefined
: undefined;
if (debug) {
log.info(`meta:`, meta);
}
const Head = module.Head;
const { component } = (await grabPageBundledReactComponent({
file_path,
@ -107,6 +109,9 @@ export default async function grabPageComponent({ req, file_path: passed_file_pa
if (!component) {
throw new Error(`Couldn't grab page component`);
}
if (debug) {
log.info(`component:`, component);
}
return {
component,
serverRes,
@ -118,6 +123,7 @@ export default async function grabPageComponent({ req, file_path: passed_file_pa
};
}
catch (error) {
console.error(`Error Grabbing Page Component: ${error.message}`);
return await grabPageErrorComponent({
error,
routeParams,

View File

@ -0,0 +1,21 @@
import grabDirNames from "../../../utils/grab-dir-names";
import path from "path";
import AppNames from "../../../utils/grab-app-names";
import { existsSync } from "fs";
export default function grabRootFile() {
const { PAGES_DIR } = grabDirNames();
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_js_file = `${path.join(PAGES_DIR, AppNames["RootPagesComponentName"])}.js`;
const root_pages_component_jsx_file = `${path.join(PAGES_DIR, AppNames["RootPagesComponentName"])}.jsx`;
const root_file = existsSync(root_pages_component_tsx_file)
? root_pages_component_tsx_file
: existsSync(root_pages_component_ts_file)
? root_pages_component_ts_file
: existsSync(root_pages_component_jsx_file)
? root_pages_component_jsx_file
: existsSync(root_pages_component_js_file)
? root_pages_component_js_file
: undefined;
return { root_file };
}

View File

@ -6,21 +6,7 @@ import { readFile } from "fs/promises";
import grabDirNames from "../../../utils/grab-dir-names";
import path from "path";
import { execSync } from "child_process";
const tailwindPlugin = {
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",
};
});
},
};
import tailwindEsbuildPlugin from "./tailwind-esbuild-plugin";
export default async function grabTsxStringModule({ tsx, file_path, }) {
const dev = isDevelopment();
const { BUNX_CWD_MODULE_CACHE_DIR } = grabDirNames();
@ -44,7 +30,7 @@ export default async function grabTsxStringModule({ tsx, file_path, }) {
"process.env.NODE_ENV": JSON.stringify(dev ? "development" : "production"),
},
metafile: true,
plugins: [tailwindPlugin],
plugins: [tailwindEsbuildPlugin],
jsx: "automatic",
write: true,
outfile: out_file_path,

View File

@ -1,55 +1,109 @@
import grabDirNames from "../../../utils/grab-dir-names";
const { BUNX_HYDRATION_SRC_DIR } = grabDirNames();
import { AppData } from "../../../data/app-data";
export default async function ({ bundledMap }) {
let script = "";
// script += `import React from "react";\n`;
// script += `import { hydrateRoot } from "react-dom/client";\n`;
// script += `import App from "${page_file}";\n`;
// script += `declare global {\n`;
// script += ` interface Window {\n`;
// script += ` ${ClientWindowPagePropsName}: any;\n`;
// script += ` }\n`;
// script += `}\n`;
// script += `let root: any = null;\n\n`;
// script += `const component = <App {...window.${ClientWindowPagePropsName}} />;\n\n`;
// script += `const container = document.getElementById("${ClientRootElementIDName}");\n\n`;
// script += `if (container) {\n`;
// script += ` root = hydrateRoot(container, component);\n`;
// script += `}\n\n`;
script += `console.log(\`Development Environment\`);\n`;
// script += `console.log(import.meta);\n`;
// script += `if (import.meta.hot) {\n`;
// script += ` console.log(\`HMR active\`);\n`;
// script += ` import.meta.hot.dispose(() => {\n`;
// script += ` console.log("dispose");\n`;
// script += ` });\n`;
// script += `}\n`;
script += `console.log(\`Development Environment\`);\n\n`;
script += `const hmr = new EventSource("/__hmr");\n`;
script += `hmr.addEventListener("update", async (event) => {\n`;
// script += ` console.log(\`HMR even received:\`, event);\n`;
script += ` if (event.data) {\n`;
script += ` console.log(\`HMR Changes Detected. Reloading ...\`);\n`;
// script += ` console.log("event", event);\n`;
// script += ` console.log("window.${ClientRootComponentWindowName}", window.${ClientRootComponentWindowName});\n\n`;
// script += ` const event_data = JSON.parse(event.data);\n\n`;
// script += ` const new_js_path = \`/\${event_data.target_map.path}\`;\n\n`;
// script += ` console.log("event_data", event_data);\n\n`;
// script += ` console.log("new_js_path", new_js_path);\n\n`;
// script += ` if (window.${ClientRootComponentWindowName}) {\n`;
// script += ` const new_component = await import(new_js_path);\n`;
// script += ` window.${ClientRootComponentWindowName}.render(new_component);\n`;
// script += ` }\n`;
// script += ` import("${page_file}?t=" + event.data.update).then((module) => {\n`;
// script += ` root.render(module.default);\n`;
// script += ` })\n`;
// script += ` console.log("root", root);\n`;
// script += ` root.unmount();\n`;
// script += ` const container = document.getElementById("${ClientRootElementIDName}");\n\n`;
// script += ` root = hydrateRoot(container!, component);\n`;
// script += ` window.history.pushState({ page: 1 }, "New Page Title", \`\${window.location.pathname}?v=\${Date.now()}\`);\n`;
// script += ` root.render(component);\n`;
script += ` window.location.reload();\n`;
script += ` console.log(\`HMR Changes Detected. Updating ...\`);\n`;
script += ` try {\n`;
script += ` const data = JSON.parse(event.data);\n`;
// script += ` console.log("data", data);\n`;
// script += ` const modulePath = \`/\${data.target_map.path}\`;\n\n`;
// script += ` const modulePath = \`/${AppData["ClientHMRPath"]}?href=\${window.location.href}&t=\${Date.now()}\`;\n\n`;
// script += ` console.log("Fetching updated module ...", modulePath);\n\n`;
// script += ` const newModule = await import(modulePath);\n\n`;
// script += ` console.log("newModule", newModule);\n\n`;
// script += ` if (window.__BUNEXT_RERENDER__ && newModule.default) {\n`;
// script += ` window.__BUNEXT_RERENDER__(newModule.default);\n`;
// script += ` console.log(\`HMR: Component updated in-place\`);\n`;
// script += ` } else {\n`;
// script += ` console.warn(\`HMR: No re-render helper found, falling back to reload\`);\n`;
// // script += ` window.location.reload();\n`;
// script += ` }\n\n`;
script += ` if (data.target_map.css_path) {\n`;
script += ` const oldLink = document.querySelector('link[rel="stylesheet"]');\n`;
script += ` const newLink = document.createElement("link");\n`;
script += ` newLink.rel = "stylesheet";\n`;
script += ` newLink.href = \`/\${data.target_map.css_path}?t=\${Date.now()}\`;\n`;
script += ` newLink.onload = () => oldLink?.remove();\n`;
script += ` document.head.appendChild(newLink);\n`;
script += ` }\n`;
script += ` const newScriptPath = \`/\${data.target_map.path}?t=\${Date.now()}\`;\n\n`;
script += ` const oldScript = document.getElementById("${AppData["BunextClientHydrationScriptID"]}");\n`;
script += ` if (oldScript) {\n`;
script += ` oldScript.remove();\n`;
script += ` }\n\n`;
script += ` const newScript = document.createElement("script");\n`;
script += ` newScript.id = "${AppData["BunextClientHydrationScriptID"]}";\n`;
script += ` newScript.type = "module";\n`;
script += ` newScript.src = newScriptPath;\n`;
// script += ` console.log("newScript", newScript);\n`;
script += ` document.head.appendChild(newScript);\n\n`;
script += ` } catch (err) {\n`;
script += ` console.error("HMR update failed, falling back to reload:", err.message);\n`;
// script += ` window.location.reload();\n`;
script += ` }\n`;
script += ` }\n`;
script += ` });\n`;
script += `});\n`;
return script;
}
// import grabDirNames from "../../../utils/grab-dir-names";
// import type { BundlerCTXMap, PageDistGenParams } from "../../../types";
// const { BUNX_HYDRATION_SRC_DIR } = grabDirNames();
// type Params = {
// bundledMap?: BundlerCTXMap;
// };
// export default async function ({ bundledMap }: Params) {
// let script = "";
// // script += `import React from "react";\n`;
// // script += `import { hydrateRoot } from "react-dom/client";\n`;
// // script += `import App from "${page_file}";\n`;
// // script += `declare global {\n`;
// // script += ` interface Window {\n`;
// // script += ` ${ClientWindowPagePropsName}: any;\n`;
// // script += ` }\n`;
// // script += `}\n`;
// // script += `let root: any = null;\n\n`;
// // script += `const component = <App {...window.${ClientWindowPagePropsName}} />;\n\n`;
// // script += `const container = document.getElementById("${ClientRootElementIDName}");\n\n`;
// // script += `if (container) {\n`;
// // script += ` root = hydrateRoot(container, component);\n`;
// // script += `}\n\n`;
// script += `console.log(\`Development Environment\`);\n`;
// // script += `console.log(import.meta);\n`;
// // script += `if (import.meta.hot) {\n`;
// // script += ` console.log(\`HMR active\`);\n`;
// // script += ` import.meta.hot.dispose(() => {\n`;
// // script += ` console.log("dispose");\n`;
// // script += ` });\n`;
// // script += `}\n`;
// script += `const hmr = new EventSource("/__hmr");\n`;
// script += `hmr.addEventListener("update", async (event) => {\n`;
// // script += ` console.log(\`HMR even received:\`, event);\n`;
// script += ` if (event.data) {\n`;
// script += ` console.log(\`HMR Changes Detected. Reloading ...\`);\n`;
// // script += ` console.log("event", event);\n`;
// // script += ` console.log("window.${ClientRootComponentWindowName}", window.${ClientRootComponentWindowName});\n\n`;
// // script += ` const event_data = JSON.parse(event.data);\n\n`;
// // script += ` const new_js_path = \`/\${event_data.target_map.path}\`;\n\n`;
// // script += ` console.log("event_data", event_data);\n\n`;
// // script += ` console.log("new_js_path", new_js_path);\n\n`;
// // script += ` if (window.${ClientRootComponentWindowName}) {\n`;
// // script += ` const new_component = await import(new_js_path);\n`;
// // script += ` window.${ClientRootComponentWindowName}.render(new_component);\n`;
// // script += ` }\n`;
// // script += ` import("${page_file}?t=" + event.data.update).then((module) => {\n`;
// // script += ` root.render(module.default);\n`;
// // script += ` })\n`;
// // script += ` console.log("root", root);\n`;
// // script += ` root.unmount();\n`;
// // script += ` const container = document.getElementById("${ClientRootElementIDName}");\n\n`;
// // script += ` root = hydrateRoot(container!, component);\n`;
// // script += ` window.history.pushState({ page: 1 }, "New Page Title", \`\${window.location.pathname}?v=\${Date.now()}\`);\n`;
// // script += ` root.render(component);\n`;
// script += ` window.location.reload();\n`;
// script += ` }\n`;
// script += ` });\n`;
// return script;
// }

View File

@ -1,7 +1,6 @@
import isDevelopment from "../../../utils/is-development";
import getCache from "../../cache/get-cache";
import writeCache from "../../cache/write-cache";
import genWebHTML from "./generate-web-html";
import generateWebPageResponseFromComponentReturn from "./generate-web-page-response-from-component-return";
import grabPageComponent from "./grab-page-component";
import grabPageErrorComponent from "./grab-page-error-component";
export default async function handleWebPages({ req, }) {
@ -20,58 +19,18 @@ export default async function handleWebPages({ req, }) {
return new Response(existing_cache, res_opts);
}
}
const componentRes = await grabPageComponent({ req });
return await generateRes(componentRes);
}
catch (error) {
const componentRes = await grabPageErrorComponent({ error });
return await generateRes(componentRes);
}
}
async function generateRes({ component, module, bundledMap, head, meta, routeParams, serverRes, }) {
const html = await genWebHTML({
component,
pageProps: serverRes,
bundledMap,
module,
meta,
head,
routeParams,
});
if (serverRes?.redirect?.destination) {
return Response.redirect(serverRes.redirect.destination, serverRes.redirect.permanent
? 301
: serverRes.redirect.status_code || 302);
}
const res_opts = {
...serverRes?.responseOptions,
headers: {
"Content-Type": "text/html",
...serverRes?.responseOptions?.headers,
},
};
if (isDevelopment()) {
res_opts.headers = {
...res_opts.headers,
"Cache-Control": "no-cache, no-store, must-revalidate",
Pragma: "no-cache",
Expires: "0",
};
}
const cache_page = module.config?.cachePage || serverRes?.cachePage || false;
const expiry_seconds = module.config?.cacheExpiry || serverRes?.cacheExpiry;
if (cache_page && routeParams?.url) {
const key = routeParams.url.pathname + (routeParams.url.search || "");
writeCache({
key,
value: html,
paradigm: "html",
expiry_seconds,
const componentRes = await grabPageComponent({
req,
});
return await generateWebPageResponseFromComponentReturn({
...componentRes,
});
}
const res = new Response(html, res_opts);
if (routeParams?.resTransform) {
return await routeParams.resTransform(res);
catch (error) {
console.error(`Error Handling Web Page: ${error.message}`);
const componentRes = await grabPageErrorComponent({
error,
});
return await generateWebPageResponseFromComponentReturn(componentRes);
}
return res;
}

View File

@ -0,0 +1,20 @@
import * as esbuild from "esbuild";
import postcss from "postcss";
import tailwindcss from "@tailwindcss/postcss";
import { readFile } from "fs/promises";
const tailwindEsbuildPlugin = {
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 tailwindEsbuildPlugin;

View File

@ -0,0 +1,106 @@
import * as esbuild from "esbuild";
import tailwindEsbuildPlugin from "./tailwind-esbuild-plugin";
import path from "path";
export default async function writeHMRTsxModule({ tsx, out_file }) {
try {
const build = await esbuild.build({
stdin: {
contents: tsx,
resolveDir: process.cwd(),
loader: "tsx",
},
bundle: true,
format: "esm",
target: "es2020",
platform: "browser",
external: [
"react",
"react-dom",
"react/jsx-runtime",
"react-dom/client",
],
minify: true,
jsx: "automatic",
outfile: out_file,
plugins: [tailwindEsbuildPlugin],
metafile: true,
});
const artifacts = Object.entries(build.metafile.outputs)
.filter(([, meta]) => meta.entryPoint)
.map(([outputPath, meta]) => {
const cssPath = meta.cssBundle || undefined;
return {
path: outputPath,
hash: path.basename(outputPath, path.extname(outputPath)),
type: outputPath.endsWith(".css")
? "text/css"
: "text/javascript",
css_path: cssPath,
};
});
return artifacts?.[0];
}
catch (error) {
return undefined;
}
}
// import * as esbuild from "esbuild";
// import path from "path";
// import tailwindEsbuildPlugin from "./tailwind-esbuild-plugin";
// const hmrExternalsPlugin: esbuild.Plugin = {
// name: "hmr-globals",
// setup(build) {
// const mapping: Record<string, string> = {
// react: "__REACT__",
// "react-dom": "__REACT_DOM__",
// "react-dom/client": "__REACT_DOM_CLIENT__",
// "react/jsx-runtime": "__JSX_RUNTIME__",
// };
// const filter = new RegExp(
// `^(${Object.keys(mapping)
// .map((k) => k.replace("/", "\\/"))
// .join("|")})$`,
// );
// build.onResolve({ filter }, (args) => {
// return { path: args.path, namespace: "hmr-global" };
// });
// build.onLoad({ filter: /.*/, namespace: "hmr-global" }, (args) => {
// const globalName = mapping[args.path];
// return {
// contents: `module.exports = window.${globalName};`,
// loader: "js",
// };
// });
// },
// };
// type Params = {
// tsx: string;
// file_path: string;
// out_file: string;
// };
// export default async function writeHMRTsxModule({
// tsx,
// file_path,
// out_file,
// }: Params) {
// try {
// await esbuild.build({
// stdin: {
// contents: tsx,
// resolveDir: path.dirname(file_path),
// loader: "tsx",
// },
// bundle: true,
// format: "esm",
// target: "es2020",
// platform: "browser",
// minify: true,
// jsx: "automatic",
// outfile: out_file,
// plugins: [hmrExternalsPlugin, tailwindEsbuildPlugin],
// });
// return true;
// } catch (error) {
// return false;
// }
// }

1
dist/index.js vendored
View File

@ -11,6 +11,7 @@ global.ORA_SPINNER.clear();
global.HMR_CONTROLLERS = [];
global.IS_FIRST_BUNDLE_READY = false;
global.BUNDLER_REBUILDS = 0;
global.PAGE_FILES = [];
await init();
const { PAGES_DIR } = grabDirNames();
const router = new Bun.FileSystemRouter({

12
dist/utils/log.js vendored
View File

@ -1,7 +1,7 @@
import chalk from "chalk";
import AppNames from "./grab-app-names";
const prefix = {
info: chalk.cyan.bold(""),
info: chalk.bgCyan.bold(" nfo "),
success: chalk.green.bold("✓"),
error: chalk.red.bold("✗"),
warn: chalk.yellow.bold("⚠"),
@ -9,12 +9,16 @@ const prefix = {
watch: chalk.blue.bold("◉"),
};
export const log = {
info: (msg) => console.log(`${prefix.info} ${chalk.white(msg)}`),
success: (msg) => console.log(`${prefix.success} ${chalk.green(msg)}`),
info: (msg, log) => {
console.log(`${prefix.info} ${chalk.white(msg)}`, log || "");
},
success: (msg, log) => {
console.log(`${prefix.success} ${chalk.green(msg)}`, log || "");
},
error: (msg) => console.error(`${prefix.error} ${chalk.red(String(msg))}`),
warn: (msg) => console.warn(`${prefix.warn} ${chalk.yellow(msg)}`),
build: (msg) => console.log(`${prefix.build} ${chalk.magenta(msg)}`),
watch: (msg) => console.log(`${prefix.watch} ${chalk.blue(msg)}`),
server: (url) => console.log(`${prefix.success} ${chalk.white("Server running on")} ${chalk.cyan.underline(url)}`),
banner: () => console.log(`\n ${chalk.cyan.bold(AppNames.name)} ${chalk.gray(`v${AppNames.version}`)}\n`),
banner: () => console.log(`\n ${chalk.cyan.bold(AppNames.name)} ${chalk.gray(`v${global.CURRENT_VERSION || AppNames["version"]}`)}\n`),
};

View File

@ -2,7 +2,7 @@
"name": "@moduletrace/bunext",
"module": "index.ts",
"type": "module",
"version": "1.0.5",
"version": "1.0.6",
"bin": {
"bunext": "dist/index.js"
},
@ -13,6 +13,7 @@
],
"scripts": {
"dev": "tsc --watch",
"publish": "tsc --noEmit && tsc && git add . && git commit -m 'Update HMR. Make it true HMR. Add URL to page server props' && git push",
"build": "tsc"
},
"devDependencies": {

View File

@ -11,6 +11,8 @@ export default function () {
log.banner();
log.info("Starting production server ...");
process.env.NODE_ENV = "production";
await init();
const config = await grabConfig();

View File

@ -2,4 +2,6 @@ export const AppData = {
DefaultCacheExpiryTimeSeconds: 60 * 60,
DefaultCronInterval: 30000,
BunextStaticFilesCacheExpiry: 60 * 60 * 24 * 7,
ClientHMRPath: "__bunext_client_hmr__",
BunextClientHydrationScriptID: "bunext-client-hydration-script",
} as const;

View File

@ -1,37 +1,16 @@
import { existsSync, writeFileSync } from "fs";
import path from "path";
import { writeFileSync } from "fs";
import * as esbuild from "esbuild";
import postcss from "postcss";
import tailwindcss from "@tailwindcss/postcss";
import { readFile } from "fs/promises";
import grabAllPages from "../../utils/grab-all-pages";
import grabDirNames from "../../utils/grab-dir-names";
import AppNames from "../../utils/grab-app-names";
import isDevelopment from "../../utils/is-development";
import type { BundlerCTXMap } from "../../types";
import { execSync } from "child_process";
import grabConstants from "../../utils/grab-constants";
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";
const { HYDRATION_DST_DIR, PAGES_DIR, HYDRATION_DST_DIR_MAP_JSON_FILE } =
grabDirNames();
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",
};
});
},
};
const { HYDRATION_DST_DIR, HYDRATION_DST_DIR_MAP_JSON_FILE } = grabDirNames();
type Params = {
watch?: boolean;
@ -41,37 +20,16 @@ type Params = {
export default async function allPagesBundler(params?: Params) {
const pages = grabAllPages({ exclude_api: true });
const { ClientRootElementIDName, ClientRootComponentWindowName } =
grabConstants();
const virtualEntries: Record<string, string> = {};
const dev = isDevelopment();
const root_component_path = path.join(
PAGES_DIR,
`${AppNames["RootPagesComponentName"]}.tsx`,
);
const does_root_exist = existsSync(root_component_path);
for (const page of pages) {
const key = page.local_path;
let txt = ``;
txt += `import { hydrateRoot } from "react-dom/client";\n`;
if (does_root_exist) {
txt += `import Root from "${root_component_path}";\n`;
}
txt += `import Page from "${page.local_path}";\n\n`;
txt += `const pageProps = window.__PAGE_PROPS__ || {};\n`;
if (does_root_exist) {
txt += `const component = <Root {...pageProps}><Page {...pageProps} /></Root>\n`;
} else {
txt += `const component = <Page {...pageProps} />\n`;
}
txt += `const root = hydrateRoot(document.getElementById("${ClientRootElementIDName}"), component);\n\n`;
txt += `window.${ClientRootComponentWindowName} = root;\n`;
const txt = grabClientHydrationScript({
page_local_path: page.local_path,
});
virtualEntries[key] = txt;
}
@ -104,48 +62,15 @@ export default async function allPagesBundler(params?: Params) {
build.onEnd((result) => {
if (result.errors.length > 0) return;
const artifacts: (BundlerCTXMap | undefined)[] = Object.entries(
result.metafile!.outputs,
)
.filter(([, meta]) => meta.entryPoint)
.map(([outputPath, meta]) => {
const target_page = pages.find((p) => {
return (
meta.entryPoint === `virtual:${p.local_path}`
);
});
const artifacts = grabArtifactsFromBundledResults({
pages,
result,
});
if (!target_page || !meta.entryPoint) {
return undefined;
}
const { file_name, local_path, url_path } = target_page;
const cssPath = meta.cssBundle || undefined;
return {
path: outputPath,
hash: path.basename(
outputPath,
path.extname(outputPath),
),
type: outputPath.endsWith(".css")
? "text/css"
: "text/javascript",
entrypoint: meta.entryPoint,
css_path: cssPath,
file_name,
local_path,
url_path,
};
});
if (artifacts.length > 0) {
const final_artifacts = artifacts.filter((a) =>
Boolean(a?.entrypoint),
) as BundlerCTXMap[];
global.BUNDLER_CTX_MAP = final_artifacts;
params?.post_build_fn?.({ artifacts: final_artifacts });
if (artifacts?.[0] && artifacts.length > 0) {
global.BUNDLER_CTX_MAP = artifacts;
global.PAGE_FILES = pages;
params?.post_build_fn?.({ artifacts });
writeFileSync(
HYDRATION_DST_DIR_MAP_JSON_FILE,
@ -154,7 +79,7 @@ export default async function allPagesBundler(params?: Params) {
}
const elapsed = (performance.now() - buildStart).toFixed(0);
log.success(`Built in ${elapsed}ms`);
log.success(`[Built] in ${elapsed}ms`);
if (params?.exit_after_first_build) {
process.exit();
@ -180,9 +105,10 @@ export default async function allPagesBundler(params?: Params) {
},
entryNames: "[dir]/[name]/[hash]",
metafile: true,
plugins: [tailwindPlugin, virtualPlugin, artifactTracker],
plugins: [tailwindEsbuildPlugin, virtualPlugin, artifactTracker],
jsx: "automatic",
splitting: true,
logLevel: "silent",
});
await ctx.rebuild();

View File

@ -0,0 +1,56 @@
import path from "path";
import * as esbuild from "esbuild";
import type { BundlerCTXMap, PageFiles } from "../../types";
type Params = {
result: esbuild.BuildResult<esbuild.BuildOptions>;
pages: PageFiles[];
};
export default function grabArtifactsFromBundledResults({
result,
pages,
}: Params) {
if (result.errors.length > 0) return;
const artifacts: (BundlerCTXMap | undefined)[] = Object.entries(
result.metafile!.outputs,
)
.filter(([, meta]) => meta.entryPoint)
.map(([outputPath, meta]) => {
const target_page = pages.find((p) => {
return meta.entryPoint === `virtual:${p.local_path}`;
});
if (!target_page || !meta.entryPoint) {
return undefined;
}
const { file_name, local_path, url_path } = target_page;
const cssPath = meta.cssBundle || undefined;
return {
path: outputPath,
hash: path.basename(outputPath, path.extname(outputPath)),
type: outputPath.endsWith(".css")
? "text/css"
: "text/javascript",
entrypoint: meta.entryPoint,
css_path: cssPath,
file_name,
local_path,
url_path,
};
});
if (artifacts.length > 0) {
const final_artifacts = artifacts.filter((a) =>
Boolean(a?.entrypoint),
) as BundlerCTXMap[];
return final_artifacts;
}
return undefined;
}

View File

@ -0,0 +1,84 @@
import { existsSync } from "fs";
import path from "path";
import grabDirNames from "../../utils/grab-dir-names";
import AppNames from "../../utils/grab-app-names";
import grabConstants from "../../utils/grab-constants";
const { PAGES_DIR } = grabDirNames();
type Params = {
page_local_path: string;
};
export default function grabClientHydrationScript({ page_local_path }: Params) {
const { ClientRootElementIDName, ClientRootComponentWindowName } =
grabConstants();
const root_component_path = path.join(
PAGES_DIR,
`${AppNames["RootPagesComponentName"]}.tsx`,
);
const does_root_exist = existsSync(root_component_path);
// let txt = ``;
// txt += `import { hydrateRoot } from "react-dom/client";\n`;
// if (does_root_exist) {
// txt += `import Root from "${root_component_path}";\n`;
// }
// txt += `import Page from "${page.local_path}";\n\n`;
// txt += `const pageProps = window.__PAGE_PROPS__ || {};\n`;
// if (does_root_exist) {
// txt += `const component = <Root {...pageProps}><Page {...pageProps} /></Root>\n`;
// } else {
// txt += `const component = <Page {...pageProps} />\n`;
// }
// txt += `const root = hydrateRoot(document.getElementById("${ClientRootElementIDName}"), component);\n\n`;
// txt += `window.${ClientRootComponentWindowName} = root;\n`;
let txt = ``;
// txt += `import * as React from "react";\n`;
// txt += `import * as ReactDOM from "react-dom";\n`;
// txt += `import * as ReactDOMClient from "react-dom/client";\n`;
// txt += `import * as JSXRuntime from "react/jsx-runtime";\n`;
txt += `import { hydrateRoot, createElement } from "react-dom/client";\n`;
if (does_root_exist) {
txt += `import Root from "${root_component_path}";\n`;
}
txt += `import Page from "${page_local_path}";\n\n`;
// txt += `window.__REACT__ = React;\n`;
// txt += `window.__REACT_DOM__ = ReactDOM;\n`;
// txt += `window.__REACT_DOM_CLIENT__ = ReactDOMClient;\n`;
// txt += `window.__JSX_RUNTIME__ = JSXRuntime;\n\n`;
txt += `const pageProps = window.__PAGE_PROPS__ || {};\n`;
if (does_root_exist) {
txt += `const component = <Root {...pageProps}><Page {...pageProps} /></Root>\n`;
} else {
txt += `const component = <Page {...pageProps} />\n`;
}
txt += `if (window.${ClientRootComponentWindowName}?.render) {\n`;
txt += ` window.${ClientRootComponentWindowName}.render(component);\n`;
txt += `} else {\n`;
txt += ` const root = hydrateRoot(document.getElementById("${ClientRootElementIDName}"), component);\n\n`;
txt += ` window.${ClientRootComponentWindowName} = root;\n`;
txt += ` window.__BUNEXT_RERENDER__ = (NewPage) => {\n`;
txt += ` const props = window.__PAGE_PROPS__ || {};\n`;
txt += ` root.render(<NewPage {...props} />);\n`;
txt += ` };\n`;
txt += `}\n`;
// // HMR re-render helper
// if (does_root_exist) {
// txt += `window.__BUNEXT_RERENDER__ = (NewPage) => {\n`;
// txt += ` const props = window.__PAGE_PROPS__ || {};\n`;
// txt += ` root.render(<Root {...props}><NewPage {...props} /></Root>);\n`;
// txt += `};\n`;
// } else {
// }
return txt;
}

View File

@ -1,6 +1,7 @@
import { existsSync, mkdirSync, statSync, writeFileSync } from "fs";
import grabDirNames from "../utils/grab-dir-names";
import { execSync } from "child_process";
import path from "path";
export default async function () {
const dirNames = grabDirNames();
@ -8,6 +9,14 @@ export default async function () {
execSync(`rm -rf ${dirNames.BUNEXT_CACHE_DIR}`);
execSync(`rm -rf ${dirNames.BUNX_CWD_MODULE_CACHE_DIR}`);
try {
const current_version = (
await Bun.file(path.resolve(__dirname, "../../package.json")).json()
).version;
global.CURRENT_VERSION = current_version;
} catch (error) {}
const keys = Object.keys(dirNames) as (keyof ReturnType<
typeof grabDirNames
>)[];

View File

@ -0,0 +1,33 @@
import type { Server } from "bun";
import grabDirNames from "../../utils/grab-dir-names";
import path from "path";
import isDevelopment from "../../utils/is-development";
import { existsSync } from "fs";
const { PUBLIC_DIR } = grabDirNames();
type Params = {
req: Request;
server: Server;
};
export default async function ({ req, server }: Params): Promise<Response> {
try {
const is_dev = isDevelopment();
const url = new URL(req.url);
const file_path = path.join(PUBLIC_DIR, url.pathname);
if (!existsSync(file_path)) {
return new Response(`File Doesn't Exist`, {
status: 404,
});
}
const file = Bun.file(file_path);
return new Response(file);
} catch (error) {
return new Response(`File Not Found`, {
status: 404,
});
}
}

View File

@ -0,0 +1,84 @@
import type { Server } from "bun";
import grabDirNames from "../../utils/grab-dir-names";
import { AppData } from "../../data/app-data";
import path from "path";
import grabRootFile from "./web-pages/grab-root-file";
import grabPageBundledReactComponent from "./web-pages/grab-page-bundled-react-component";
import writeHMRTsxModule from "./web-pages/write-hmr-tsx-module";
const { PUBLIC_DIR, BUNX_HYDRATION_SRC_DIR } = grabDirNames();
type Params = {
req: Request;
server: Server;
};
export default async function ({ req, server }: Params): Promise<Response> {
try {
const url = new URL(req.url);
const target_href = url.searchParams.get("href");
if (!target_href) {
return new Response(
`No HREF passed to /${AppData["ClientHMRPath"]}`,
{ status: 404 },
);
}
const target_href_url = new URL(target_href);
const match = global.ROUTER.match(target_href_url.pathname);
if (!match?.filePath) {
return new Response(`No pages file matched for this path`, {
status: 404,
});
}
const out_file = path.join(
BUNX_HYDRATION_SRC_DIR,
target_href_url.pathname,
"index.js",
);
const { root_file } = grabRootFile();
const { tsx } =
(await grabPageBundledReactComponent({
file_path: match.filePath,
root_file,
})) || {};
if (!tsx) {
throw new Error(`Couldn't grab txt string`);
}
const artifact = await writeHMRTsxModule({
tsx,
out_file,
});
const file = Bun.file(out_file);
if (await file.exists()) {
return new Response(file, {
headers: {
"Content-Type": "text/javascript",
},
});
}
return new Response("Not found", {
status: 404,
});
} catch (error: any) {
const error_msg = error.message;
console.error(error_msg);
return new Response(error_msg || "HMR Error", {
status: 404,
});
}
}

View File

@ -0,0 +1,50 @@
import type { Server } from "bun";
import type { BunextServerRouteConfig, BunxRouteParams } from "../../types";
import grabRouteParams from "../../utils/grab-route-params";
import grabConstants from "../../utils/grab-constants";
import grabRouter from "../../utils/grab-router";
type Params = {
req: Request;
server: Server;
};
export default async function ({ req, server }: Params): Promise<Response> {
const referer_url = new URL(req.headers.get("referer") || "");
const match = global.ROUTER.match(referer_url.pathname);
const target_map = match?.filePath
? global.BUNDLER_CTX_MAP?.find((m) => m.local_path == match.filePath)
: undefined;
let controller: ReadableStreamDefaultController<string>;
const stream = new ReadableStream<string>({
start(c) {
controller = c;
global.HMR_CONTROLLERS.push({
controller: c,
page_url: referer_url.href,
target_map,
});
},
cancel() {
const targetControllerIndex = global.HMR_CONTROLLERS.findIndex(
(c) => c.controller == controller,
);
if (
typeof targetControllerIndex == "number" &&
targetControllerIndex >= 0
) {
global.HMR_CONTROLLERS.splice(targetControllerIndex, 1);
}
},
});
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
Connection: "keep-alive",
},
});
}

View File

@ -0,0 +1,40 @@
import type { Server } from "bun";
import grabDirNames from "../../utils/grab-dir-names";
import path from "path";
import isDevelopment from "../../utils/is-development";
import { existsSync } from "fs";
const { PUBLIC_DIR, BUNX_HYDRATION_SRC_DIR } = grabDirNames();
type Params = {
req: Request;
server: Server;
};
export default async function ({ req, server }: Params): Promise<Response> {
try {
const is_dev = isDevelopment();
const url = new URL(req.url);
const file_path = path.join(
PUBLIC_DIR,
url.pathname.replace(/^\/public/, ""),
);
if (!existsSync(file_path)) {
return new Response(`Public File Doesn't Exist`, {
status: 404,
});
}
const file = Bun.file(file_path);
let res_opts: ResponseInit = {};
return new Response(file, res_opts);
} catch (error) {
return new Response(`Public File Not Found`, {
status: 404,
});
}
}

View File

@ -1,13 +1,14 @@
import path from "path";
import type { ServeOptions } from "bun";
import grabAppPort from "../../utils/grab-app-port";
import grabDirNames from "../../utils/grab-dir-names";
import handleWebPages from "./web-pages/handle-web-pages";
import handleRoutes from "./handle-routes";
import isDevelopment from "../../utils/is-development";
import grabConstants from "../../utils/grab-constants";
import { AppData } from "../../data/app-data";
import { existsSync } from "fs";
import handleHmr from "./handle-hmr";
import handleHmrUpdate from "./handle-hmr-update";
import handlePublic from "./handle-public";
import handleFiles from "./handle-files";
type Params = {
dev?: boolean;
@ -15,7 +16,6 @@ type Params = {
export default async function (params?: Params): Promise<ServeOptions> {
const port = grabAppPort();
const { PUBLIC_DIR } = grabDirNames();
const is_dev = isDevelopment();
@ -26,6 +26,8 @@ export default async function (params?: Params): Promise<ServeOptions> {
const { config } = grabConstants();
let response: Response | undefined = undefined;
if (config?.middleware) {
const middleware_res = await config.middleware({
req,
@ -38,109 +40,32 @@ export default async function (params?: Params): Promise<ServeOptions> {
}
}
if (url.pathname === "/__hmr" && is_dev) {
const referer_url = new URL(
req.headers.get("referer") || "",
if (url.pathname == `/${AppData["ClientHMRPath"]}`) {
response = await handleHmrUpdate({ req, server });
} else if (url.pathname === "/__hmr" && is_dev) {
response = await handleHmr({ req, server });
} else if (url.pathname.startsWith("/api/")) {
response = await handleRoutes({ req, server });
} else if (url.pathname.startsWith("/public/")) {
response = await handlePublic({ req, server });
} else if (url.pathname.match(/\..*$/)) {
response = await handleFiles({ req, server });
} else {
response = await handleWebPages({ req });
}
if (!response) {
throw new Error(`No Response generated`);
}
if (is_dev) {
response.headers.set(
"Cache-Control",
"no-cache, no-store, must-revalidate",
);
const match = global.ROUTER.match(referer_url.pathname);
const target_map = match?.filePath
? global.BUNDLER_CTX_MAP?.find(
(m) => m.local_path == match.filePath,
)
: undefined;
let controller: ReadableStreamDefaultController<string>;
const stream = new ReadableStream<string>({
start(c) {
controller = c;
global.HMR_CONTROLLERS.push({
controller: c,
page_url: referer_url.href,
target_map,
});
},
cancel() {
const targetControllerIndex =
global.HMR_CONTROLLERS.findIndex(
(c) => c.controller == controller,
);
if (
typeof targetControllerIndex == "number" &&
targetControllerIndex >= 0
) {
global.HMR_CONTROLLERS.splice(
targetControllerIndex,
1,
);
}
},
});
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
});
}
if (url.pathname.startsWith("/api/")) {
return await handleRoutes({ req, server });
}
if (url.pathname.startsWith("/public/")) {
try {
const file_path = path.join(
PUBLIC_DIR,
url.pathname.replace(/^\/public/, ""),
);
if (!existsSync(file_path)) {
return new Response(`Public File Doesn't Exist`, {
status: 404,
});
}
const file = Bun.file(file_path);
let res_opts: ResponseInit = {};
if (!is_dev && url.pathname.match(/__bunext/)) {
res_opts.headers = {
"Cache-Control": `public, max-age=${AppData["BunextStaticFilesCacheExpiry"]}, must-revalidate`,
};
}
return new Response(file, res_opts);
} catch (error) {
return new Response(`Public File Not Found`, {
status: 404,
});
}
}
// if (url.pathname.startsWith("/favicon.") ) {
if (url.pathname.match(/\..*$/)) {
try {
const file_path = path.join(PUBLIC_DIR, url.pathname);
if (!existsSync(file_path)) {
return new Response(`File Doesn't Exist`, {
status: 404,
});
}
const file = Bun.file(file_path);
return new Response(file);
} catch (error) {
return new Response(`File Not Found`, { status: 404 });
}
}
return await handleWebPages({ req });
return response;
} catch (error: any) {
return new Response(`Server Error: ${error.message}`, {
status: 500,

View File

@ -7,7 +7,7 @@ import { log } from "../../utils/log";
const { SRC_DIR } = grabDirNames();
export default function watcher() {
watch(
const pages_src_watcher = watch(
SRC_DIR,
{
recursive: true,
@ -17,6 +17,7 @@ export default function watcher() {
if (!filename) return;
if (event !== "rename") return;
if (!filename.match(/^pages\//)) return;
if (global.RECOMPILING) return;
@ -34,6 +35,13 @@ export default function watcher() {
} finally {
global.RECOMPILING = false;
}
if (global.PAGES_SRC_WATCHER) {
global.PAGES_SRC_WATCHER.close();
watcher();
}
},
);
global.PAGES_SRC_WATCHER = pages_src_watcher;
}

View File

@ -5,6 +5,8 @@ import type { LivePageDistGenParams } from "../../../types";
import isDevelopment from "../../../utils/is-development";
import grabWebPageHydrationScript from "./grab-web-page-hydration-script";
import grabWebMetaHTML from "./grab-web-meta-html";
import { log } from "../../../utils/log";
import { AppData } from "../../../data/app-data";
export default async function genWebHTML({
component,
@ -14,6 +16,7 @@ export default async function genWebHTML({
module,
meta,
routeParams,
debug,
}: LivePageDistGenParams) {
const { ClientRootElementIDName, ClientWindowPagePropsName } =
grabContants();
@ -22,7 +25,16 @@ export default async function genWebHTML({
path.join(process.cwd(), "node_modules", "react-dom", "server")
);
if (debug) {
log.info("component", component);
}
const componentHTML = renderToString(component);
if (debug) {
log.info("componentHTML", componentHTML);
}
const headHTML = Head
? renderToString(<Head serverRes={pageProps} ctx={routeParams} />)
: "";
@ -46,7 +58,7 @@ export default async function genWebHTML({
}</script>\n`;
if (bundledMap?.path) {
html += ` <script src="/${bundledMap.path}" type="module" async></script>\n`;
html += ` <script src="/${bundledMap.path}" type="module" id="${AppData["BunextClientHydrationScriptID"]}" async></script>\n`;
}
if (isDevelopment()) {

View File

@ -0,0 +1,79 @@
import type { GrabPageComponentRes } from "../../../types";
import isDevelopment from "../../../utils/is-development";
import { log } from "../../../utils/log";
import writeCache from "../../cache/write-cache";
import genWebHTML from "./generate-web-html";
export default async function generateWebPageResponseFromComponentReturn({
component,
module,
bundledMap,
head,
meta,
routeParams,
serverRes,
debug,
}: GrabPageComponentRes) {
const html = await genWebHTML({
component,
pageProps: serverRes,
bundledMap,
module,
meta,
head,
routeParams,
debug,
});
if (debug) {
log.info("html", html);
}
if (serverRes?.redirect?.destination) {
return Response.redirect(
serverRes.redirect.destination,
serverRes.redirect.permanent
? 301
: serverRes.redirect.status_code || 302,
);
}
const res_opts: ResponseInit = {
...serverRes?.responseOptions,
headers: {
"Content-Type": "text/html",
...serverRes?.responseOptions?.headers,
},
};
if (isDevelopment()) {
res_opts.headers = {
...res_opts.headers,
"Cache-Control": "no-cache, no-store, must-revalidate",
Pragma: "no-cache",
Expires: "0",
};
}
const cache_page =
module.config?.cachePage || serverRes?.cachePage || false;
const expiry_seconds = module.config?.cacheExpiry || serverRes?.cacheExpiry;
if (cache_page && routeParams?.url) {
const key = routeParams.url.pathname + (routeParams.url.search || "");
writeCache({
key,
value: html,
paradigm: "html",
expiry_seconds,
});
}
const res = new Response(html, res_opts);
if (routeParams?.resTransform) {
return await routeParams.resTransform(res);
}
return res;
}

View File

@ -5,37 +5,22 @@ import tailwindcss from "@tailwindcss/postcss";
import { readFile } from "fs/promises";
import grabDirNames from "../../../utils/grab-dir-names";
import path from "path";
import tailwindEsbuildPlugin from "./tailwind-esbuild-plugin";
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",
};
});
},
out_file?: string;
};
export default async function grabFilePathModule<T extends any = any>({
file_path,
out_file,
}: 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`,
);
const target_cache_file_path =
out_file ||
path.join(BUNX_CWD_MODULE_CACHE_DIR, `${path.basename(file_path)}.js`);
await esbuild.build({
entryPoints: [file_path],
@ -51,7 +36,7 @@ export default async function grabFilePathModule<T extends any = any>({
),
},
metafile: true,
plugins: [tailwindPlugin],
plugins: [tailwindEsbuildPlugin],
jsx: "automatic",
outfile: target_cache_file_path,
});

View File

@ -30,9 +30,9 @@ export default async function grabPageBundledReactComponent({
tsx += `const props = JSON.parse("${server_res_json}")\n\n`;
tsx += ` return (\n`;
if (root_file) {
tsx += ` <Root {...props}><Page {...props} /></Root>\n`;
tsx += ` <Root suppressHydrationWarning={true} {...props}><Page {...props} /></Root>\n`;
} else {
tsx += ` <Page {...props} />\n`;
tsx += ` <Page suppressHydrationWarning={true} {...props} />\n`;
}
tsx += ` )\n`;
tsx += `}\n`;
@ -44,6 +44,7 @@ export default async function grabPageBundledReactComponent({
return {
component,
server_res,
tsx,
};
} catch (error: any) {
return undefined;

View File

@ -1,5 +1,4 @@
import type { FC } from "react";
import grabDirNames from "../../../utils/grab-dir-names";
import grabRouteParams from "../../../utils/grab-route-params";
import type {
BunextPageModule,
@ -7,29 +6,28 @@ import type {
BunxRouteParams,
GrabPageComponentRes,
} from "../../../types";
import path from "path";
import AppNames from "../../../utils/grab-app-names";
import { existsSync } from "fs";
import grabPageErrorComponent from "./grab-page-error-component";
import grabPageBundledReactComponent from "./grab-page-bundled-react-component";
import _ from "lodash";
import { log } from "../../../utils/log";
import grabRootFile from "./grab-root-file";
class NotFoundError extends Error {}
type Params = {
req?: Request;
file_path?: string;
debug?: boolean;
};
export default async function grabPageComponent({
req,
file_path: passed_file_path,
debug,
}: Params): Promise<GrabPageComponentRes> {
const url = req?.url ? new URL(req.url) : undefined;
const router = global.ROUTER;
const { PAGES_DIR } = grabDirNames();
let routeParams: BunxRouteParams | undefined = undefined;
try {
@ -41,6 +39,10 @@ export default async function grabPageComponent({
url_path += url.search;
}
if (debug) {
log.info(`url_path:`, url_path);
}
const match = url_path ? router.match(url_path) : undefined;
if (!match?.filePath && url?.pathname) {
@ -49,6 +51,10 @@ export default async function grabPageComponent({
const file_path = match?.filePath || passed_file_path;
if (debug) {
log.info(`file_path:`, file_path);
}
if (!file_path) {
const errMsg = `No File Path (\`file_path\`) or Request Object (\`req\`) provided not found`;
// console.error(errMsg);
@ -65,25 +71,18 @@ export default async function grabPageComponent({
throw new Error(errMsg);
}
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_js_file = `${path.join(PAGES_DIR, AppNames["RootPagesComponentName"])}.js`;
const root_pages_component_jsx_file = `${path.join(PAGES_DIR, AppNames["RootPagesComponentName"])}.jsx`;
if (debug) {
log.info(`bundledMap:`, bundledMap);
}
const root_file = existsSync(root_pages_component_tsx_file)
? root_pages_component_tsx_file
: existsSync(root_pages_component_ts_file)
? root_pages_component_ts_file
: existsSync(root_pages_component_jsx_file)
? root_pages_component_jsx_file
: existsSync(root_pages_component_js_file)
? root_pages_component_js_file
: undefined;
const now = Date.now();
const { root_file } = grabRootFile();
const module: BunextPageModule = await import(file_path);
if (debug) {
log.info(`module:`, module);
}
const serverRes: BunextPageModuleServerReturn = await (async () => {
const default_props: BunextPageModuleServerReturn = {
url: {
@ -123,6 +122,10 @@ export default async function grabPageComponent({
}
})();
if (debug) {
log.info(`serverRes:`, serverRes);
}
const meta = module.meta
? typeof module.meta == "function" && routeParams
? await module.meta({
@ -134,6 +137,10 @@ export default async function grabPageComponent({
: undefined
: undefined;
if (debug) {
log.info(`meta:`, meta);
}
const Head = module.Head as FC<any>;
const { component } =
@ -147,6 +154,10 @@ export default async function grabPageComponent({
throw new Error(`Couldn't grab page component`);
}
if (debug) {
log.info(`component:`, component);
}
return {
component,
serverRes,
@ -157,6 +168,8 @@ export default async function grabPageComponent({
head: Head,
};
} catch (error: any) {
console.error(`Error Grabbing Page Component: ${error.message}`);
return await grabPageErrorComponent({
error,
routeParams,

View File

@ -0,0 +1,25 @@
import grabDirNames from "../../../utils/grab-dir-names";
import path from "path";
import AppNames from "../../../utils/grab-app-names";
import { existsSync } from "fs";
export default function grabRootFile() {
const { PAGES_DIR } = grabDirNames();
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_js_file = `${path.join(PAGES_DIR, AppNames["RootPagesComponentName"])}.js`;
const root_pages_component_jsx_file = `${path.join(PAGES_DIR, AppNames["RootPagesComponentName"])}.jsx`;
const root_file = existsSync(root_pages_component_tsx_file)
? root_pages_component_tsx_file
: existsSync(root_pages_component_ts_file)
? root_pages_component_ts_file
: existsSync(root_pages_component_jsx_file)
? root_pages_component_jsx_file
: existsSync(root_pages_component_js_file)
? root_pages_component_js_file
: undefined;
return { root_file };
}

View File

@ -6,29 +6,13 @@ import { readFile } from "fs/promises";
import grabDirNames from "../../../utils/grab-dir-names";
import path from "path";
import { execSync } from "child_process";
import tailwindEsbuildPlugin from "./tailwind-esbuild-plugin";
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,
@ -63,7 +47,7 @@ export default async function grabTsxStringModule<T extends any = any>({
),
},
metafile: true,
plugins: [tailwindPlugin],
plugins: [tailwindEsbuildPlugin],
jsx: "automatic",
write: true,
outfile: out_file_path,

View File

@ -1,7 +1,5 @@
import grabDirNames from "../../../utils/grab-dir-names";
import type { BundlerCTXMap, PageDistGenParams } from "../../../types";
const { BUNX_HYDRATION_SRC_DIR } = grabDirNames();
import type { BundlerCTXMap } from "../../../types";
import { AppData } from "../../../data/app-data";
type Params = {
bundledMap?: BundlerCTXMap;
@ -10,62 +8,127 @@ type Params = {
export default async function ({ bundledMap }: Params) {
let script = "";
// script += `import React from "react";\n`;
// script += `import { hydrateRoot } from "react-dom/client";\n`;
// script += `import App from "${page_file}";\n`;
// script += `declare global {\n`;
// script += ` interface Window {\n`;
// script += ` ${ClientWindowPagePropsName}: any;\n`;
// script += ` }\n`;
// script += `}\n`;
// script += `let root: any = null;\n\n`;
// script += `const component = <App {...window.${ClientWindowPagePropsName}} />;\n\n`;
// script += `const container = document.getElementById("${ClientRootElementIDName}");\n\n`;
// script += `if (container) {\n`;
// script += ` root = hydrateRoot(container, component);\n`;
// script += `}\n\n`;
script += `console.log(\`Development Environment\`);\n`;
// script += `console.log(import.meta);\n`;
// script += `if (import.meta.hot) {\n`;
// script += ` console.log(\`HMR active\`);\n`;
// script += ` import.meta.hot.dispose(() => {\n`;
// script += ` console.log("dispose");\n`;
// script += ` });\n`;
// script += `}\n`;
script += `console.log(\`Development Environment\`);\n\n`;
script += `const hmr = new EventSource("/__hmr");\n`;
script += `hmr.addEventListener("update", async (event) => {\n`;
// script += ` console.log(\`HMR even received:\`, event);\n`;
script += ` if (event.data) {\n`;
script += ` console.log(\`HMR Changes Detected. Reloading ...\`);\n`;
// script += ` console.log("event", event);\n`;
// script += ` console.log("window.${ClientRootComponentWindowName}", window.${ClientRootComponentWindowName});\n\n`;
// script += ` const event_data = JSON.parse(event.data);\n\n`;
// script += ` const new_js_path = \`/\${event_data.target_map.path}\`;\n\n`;
script += ` console.log(\`HMR Changes Detected. Updating ...\`);\n`;
script += ` try {\n`;
script += ` const data = JSON.parse(event.data);\n`;
// script += ` console.log("data", data);\n`;
// script += ` const modulePath = \`/\${data.target_map.path}\`;\n\n`;
// script += ` console.log("event_data", event_data);\n\n`;
// script += ` console.log("new_js_path", new_js_path);\n\n`;
// script += ` const modulePath = \`/${AppData["ClientHMRPath"]}?href=\${window.location.href}&t=\${Date.now()}\`;\n\n`;
// script += ` console.log("Fetching updated module ...", modulePath);\n\n`;
// script += ` const newModule = await import(modulePath);\n\n`;
// script += ` console.log("newModule", newModule);\n\n`;
// script += ` if (window.__BUNEXT_RERENDER__ && newModule.default) {\n`;
// script += ` window.__BUNEXT_RERENDER__(newModule.default);\n`;
// script += ` console.log(\`HMR: Component updated in-place\`);\n`;
// script += ` } else {\n`;
// script += ` console.warn(\`HMR: No re-render helper found, falling back to reload\`);\n`;
// // script += ` window.location.reload();\n`;
// script += ` }\n\n`;
// script += ` if (window.${ClientRootComponentWindowName}) {\n`;
// script += ` const new_component = await import(new_js_path);\n`;
// script += ` window.${ClientRootComponentWindowName}.render(new_component);\n`;
// script += ` }\n`;
script += ` if (data.target_map.css_path) {\n`;
script += ` const oldLink = document.querySelector('link[rel="stylesheet"]');\n`;
script += ` const newLink = document.createElement("link");\n`;
script += ` newLink.rel = "stylesheet";\n`;
script += ` newLink.href = \`/\${data.target_map.css_path}?t=\${Date.now()}\`;\n`;
script += ` newLink.onload = () => oldLink?.remove();\n`;
script += ` document.head.appendChild(newLink);\n`;
script += ` }\n`;
// script += ` import("${page_file}?t=" + event.data.update).then((module) => {\n`;
// script += ` root.render(module.default);\n`;
// script += ` })\n`;
// script += ` console.log("root", root);\n`;
// script += ` root.unmount();\n`;
// script += ` const container = document.getElementById("${ClientRootElementIDName}");\n\n`;
// script += ` root = hydrateRoot(container!, component);\n`;
// script += ` window.history.pushState({ page: 1 }, "New Page Title", \`\${window.location.pathname}?v=\${Date.now()}\`);\n`;
// script += ` root.render(component);\n`;
script += ` window.location.reload();\n`;
script += ` const newScriptPath = \`/\${data.target_map.path}?t=\${Date.now()}\`;\n\n`;
script += ` const oldScript = document.getElementById("${AppData["BunextClientHydrationScriptID"]}");\n`;
script += ` if (oldScript) {\n`;
script += ` oldScript.remove();\n`;
script += ` }\n\n`;
script += ` const newScript = document.createElement("script");\n`;
script += ` newScript.id = "${AppData["BunextClientHydrationScriptID"]}";\n`;
script += ` newScript.type = "module";\n`;
script += ` newScript.src = newScriptPath;\n`;
// script += ` console.log("newScript", newScript);\n`;
script += ` document.head.appendChild(newScript);\n\n`;
script += ` } catch (err) {\n`;
script += ` console.error("HMR update failed, falling back to reload:", err.message);\n`;
// script += ` window.location.reload();\n`;
script += ` }\n`;
script += ` }\n`;
script += ` });\n`;
script += `});\n`;
return script;
}
// import grabDirNames from "../../../utils/grab-dir-names";
// import type { BundlerCTXMap, PageDistGenParams } from "../../../types";
// const { BUNX_HYDRATION_SRC_DIR } = grabDirNames();
// type Params = {
// bundledMap?: BundlerCTXMap;
// };
// export default async function ({ bundledMap }: Params) {
// let script = "";
// // script += `import React from "react";\n`;
// // script += `import { hydrateRoot } from "react-dom/client";\n`;
// // script += `import App from "${page_file}";\n`;
// // script += `declare global {\n`;
// // script += ` interface Window {\n`;
// // script += ` ${ClientWindowPagePropsName}: any;\n`;
// // script += ` }\n`;
// // script += `}\n`;
// // script += `let root: any = null;\n\n`;
// // script += `const component = <App {...window.${ClientWindowPagePropsName}} />;\n\n`;
// // script += `const container = document.getElementById("${ClientRootElementIDName}");\n\n`;
// // script += `if (container) {\n`;
// // script += ` root = hydrateRoot(container, component);\n`;
// // script += `}\n\n`;
// script += `console.log(\`Development Environment\`);\n`;
// // script += `console.log(import.meta);\n`;
// // script += `if (import.meta.hot) {\n`;
// // script += ` console.log(\`HMR active\`);\n`;
// // script += ` import.meta.hot.dispose(() => {\n`;
// // script += ` console.log("dispose");\n`;
// // script += ` });\n`;
// // script += `}\n`;
// script += `const hmr = new EventSource("/__hmr");\n`;
// script += `hmr.addEventListener("update", async (event) => {\n`;
// // script += ` console.log(\`HMR even received:\`, event);\n`;
// script += ` if (event.data) {\n`;
// script += ` console.log(\`HMR Changes Detected. Reloading ...\`);\n`;
// // script += ` console.log("event", event);\n`;
// // script += ` console.log("window.${ClientRootComponentWindowName}", window.${ClientRootComponentWindowName});\n\n`;
// // script += ` const event_data = JSON.parse(event.data);\n\n`;
// // script += ` const new_js_path = \`/\${event_data.target_map.path}\`;\n\n`;
// // script += ` console.log("event_data", event_data);\n\n`;
// // script += ` console.log("new_js_path", new_js_path);\n\n`;
// // script += ` if (window.${ClientRootComponentWindowName}) {\n`;
// // script += ` const new_component = await import(new_js_path);\n`;
// // script += ` window.${ClientRootComponentWindowName}.render(new_component);\n`;
// // script += ` }\n`;
// // script += ` import("${page_file}?t=" + event.data.update).then((module) => {\n`;
// // script += ` root.render(module.default);\n`;
// // script += ` })\n`;
// // script += ` console.log("root", root);\n`;
// // script += ` root.unmount();\n`;
// // script += ` const container = document.getElementById("${ClientRootElementIDName}");\n\n`;
// // script += ` root = hydrateRoot(container!, component);\n`;
// // script += ` window.history.pushState({ page: 1 }, "New Page Title", \`\${window.location.pathname}?v=\${Date.now()}\`);\n`;
// // script += ` root.render(component);\n`;
// script += ` window.location.reload();\n`;
// script += ` }\n`;
// script += ` });\n`;
// return script;
// }

View File

@ -1,8 +1,6 @@
import type { GrabPageComponentRes } from "../../../types";
import isDevelopment from "../../../utils/is-development";
import getCache from "../../cache/get-cache";
import writeCache from "../../cache/write-cache";
import genWebHTML from "./generate-web-html";
import generateWebPageResponseFromComponentReturn from "./generate-web-page-response-from-component-return";
import grabPageComponent from "./grab-page-component";
import grabPageErrorComponent from "./grab-page-error-component";
@ -32,78 +30,20 @@ export default async function handleWebPages({
}
}
const componentRes = await grabPageComponent({ req });
return await generateRes(componentRes);
} catch (error: any) {
const componentRes = await grabPageErrorComponent({ error });
return await generateRes(componentRes);
}
}
async function generateRes({
component,
module,
bundledMap,
head,
meta,
routeParams,
serverRes,
}: GrabPageComponentRes) {
const html = await genWebHTML({
component,
pageProps: serverRes,
bundledMap,
module,
meta,
head,
routeParams,
});
if (serverRes?.redirect?.destination) {
return Response.redirect(
serverRes.redirect.destination,
serverRes.redirect.permanent
? 301
: serverRes.redirect.status_code || 302,
);
}
const res_opts: ResponseInit = {
...serverRes?.responseOptions,
headers: {
"Content-Type": "text/html",
...serverRes?.responseOptions?.headers,
},
};
if (isDevelopment()) {
res_opts.headers = {
...res_opts.headers,
"Cache-Control": "no-cache, no-store, must-revalidate",
Pragma: "no-cache",
Expires: "0",
};
}
const cache_page =
module.config?.cachePage || serverRes?.cachePage || false;
const expiry_seconds = module.config?.cacheExpiry || serverRes?.cacheExpiry;
if (cache_page && routeParams?.url) {
const key = routeParams.url.pathname + (routeParams.url.search || "");
writeCache({
key,
value: html,
paradigm: "html",
expiry_seconds,
const componentRes = await grabPageComponent({
req,
});
return await generateWebPageResponseFromComponentReturn({
...componentRes,
});
} catch (error: any) {
console.error(`Error Handling Web Page: ${error.message}`);
const componentRes = await grabPageErrorComponent({
error,
});
return await generateWebPageResponseFromComponentReturn(componentRes);
}
const res = new Response(html, res_opts);
if (routeParams?.resTransform) {
return await routeParams.resTransform(res);
}
return res;
}

View File

@ -0,0 +1,23 @@
import * as esbuild from "esbuild";
import postcss from "postcss";
import tailwindcss from "@tailwindcss/postcss";
import { readFile } from "fs/promises";
const tailwindEsbuildPlugin: 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 tailwindEsbuildPlugin;

View File

@ -0,0 +1,126 @@
import * as esbuild from "esbuild";
import tailwindEsbuildPlugin from "./tailwind-esbuild-plugin";
import type { BundlerCTXMap } from "../../../types";
import path from "path";
type Params = {
tsx: string;
out_file: string;
};
export default async function writeHMRTsxModule({ tsx, out_file }: Params) {
try {
const build = await esbuild.build({
stdin: {
contents: tsx,
resolveDir: process.cwd(),
loader: "tsx",
},
bundle: true,
format: "esm",
target: "es2020",
platform: "browser",
external: [
"react",
"react-dom",
"react/jsx-runtime",
"react-dom/client",
],
minify: true,
jsx: "automatic",
outfile: out_file,
plugins: [tailwindEsbuildPlugin],
metafile: true,
});
const artifacts: (
| Pick<BundlerCTXMap, "path" | "hash" | "css_path" | "type">
| undefined
)[] = Object.entries(build.metafile!.outputs)
.filter(([, meta]) => meta.entryPoint)
.map(([outputPath, meta]) => {
const cssPath = meta.cssBundle || undefined;
return {
path: outputPath,
hash: path.basename(outputPath, path.extname(outputPath)),
type: outputPath.endsWith(".css")
? "text/css"
: "text/javascript",
css_path: cssPath,
};
});
return artifacts?.[0];
} catch (error) {
return undefined;
}
}
// import * as esbuild from "esbuild";
// import path from "path";
// import tailwindEsbuildPlugin from "./tailwind-esbuild-plugin";
// const hmrExternalsPlugin: esbuild.Plugin = {
// name: "hmr-globals",
// setup(build) {
// const mapping: Record<string, string> = {
// react: "__REACT__",
// "react-dom": "__REACT_DOM__",
// "react-dom/client": "__REACT_DOM_CLIENT__",
// "react/jsx-runtime": "__JSX_RUNTIME__",
// };
// const filter = new RegExp(
// `^(${Object.keys(mapping)
// .map((k) => k.replace("/", "\\/"))
// .join("|")})$`,
// );
// build.onResolve({ filter }, (args) => {
// return { path: args.path, namespace: "hmr-global" };
// });
// build.onLoad({ filter: /.*/, namespace: "hmr-global" }, (args) => {
// const globalName = mapping[args.path];
// return {
// contents: `module.exports = window.${globalName};`,
// loader: "js",
// };
// });
// },
// };
// type Params = {
// tsx: string;
// file_path: string;
// out_file: string;
// };
// export default async function writeHMRTsxModule({
// tsx,
// file_path,
// out_file,
// }: Params) {
// try {
// await esbuild.build({
// stdin: {
// contents: tsx,
// resolveDir: path.dirname(file_path),
// loader: "tsx",
// },
// bundle: true,
// format: "esm",
// target: "es2020",
// platform: "browser",
// minify: true,
// jsx: "automatic",
// outfile: out_file,
// plugins: [hmrExternalsPlugin, tailwindEsbuildPlugin],
// });
// return true;
// } catch (error) {
// return false;
// }
// }

View File

@ -8,12 +8,14 @@ import type {
BundlerCTXMap,
BunextConfig,
GlobalHMRControllerObject,
PageFiles,
} from "./types";
import type { FileSystemRouter, Server } from "bun";
import init from "./functions/init";
import grabDirNames from "./utils/grab-dir-names";
import build from "./commands/build";
import type { BuildContext } from "esbuild";
import type { FSWatcher } from "fs";
/**
* # Declare Global Variables
@ -31,6 +33,9 @@ declare global {
var BUNDLER_CTX_MAP: BundlerCTXMap[] | undefined;
var IS_FIRST_BUNDLE_READY: boolean;
var BUNDLER_REBUILDS: 0;
var PAGES_SRC_WATCHER: FSWatcher | undefined;
var CURRENT_VERSION: string | undefined;
var PAGE_FILES: PageFiles[];
}
global.ORA_SPINNER = ora();
@ -38,6 +43,7 @@ global.ORA_SPINNER.clear();
global.HMR_CONTROLLERS = [];
global.IS_FIRST_BUNDLE_READY = false;
global.BUNDLER_REBUILDS = 0;
global.PAGE_FILES = [];
await init();

View File

@ -146,6 +146,7 @@ export type LivePageDistGenParams = {
bundledMap?: BundlerCTXMap;
meta?: BunextPageModuleMeta;
routeParams?: BunxRouteParams;
debug?: boolean;
};
export type BunextPageHeadFCProps = {
@ -244,11 +245,13 @@ export type GrabPageComponentRes = {
module: BunextPageModule;
meta?: BunextPageModuleMeta;
head?: FC<BunextPageHeadFCProps>;
debug?: boolean;
};
export type GrabPageReactBundledComponentRes = {
component: JSX.Element;
server_res?: BunextPageModuleServerReturn;
tsx?: string;
};
export type PageFiles = {

View File

@ -2,7 +2,7 @@ import chalk from "chalk";
import AppNames from "./grab-app-names";
const prefix = {
info: chalk.cyan.bold(""),
info: chalk.bgCyan.bold(" nfo "),
success: chalk.green.bold("✓"),
error: chalk.red.bold("✗"),
warn: chalk.yellow.bold("⚠"),
@ -11,24 +11,24 @@ const prefix = {
};
export const log = {
info: (msg: string) =>
console.log(`${prefix.info} ${chalk.white(msg)}`),
success: (msg: string) =>
console.log(`${prefix.success} ${chalk.green(msg)}`),
info: (msg: string, log?: any) => {
console.log(`${prefix.info} ${chalk.white(msg)}`, log || "");
},
success: (msg: string, log?: any) => {
console.log(`${prefix.success} ${chalk.green(msg)}`, log || "");
},
error: (msg: string | Error) =>
console.error(`${prefix.error} ${chalk.red(String(msg))}`),
warn: (msg: string) =>
console.warn(`${prefix.warn} ${chalk.yellow(msg)}`),
warn: (msg: string) => console.warn(`${prefix.warn} ${chalk.yellow(msg)}`),
build: (msg: string) =>
console.log(`${prefix.build} ${chalk.magenta(msg)}`),
watch: (msg: string) =>
console.log(`${prefix.watch} ${chalk.blue(msg)}`),
watch: (msg: string) => console.log(`${prefix.watch} ${chalk.blue(msg)}`),
server: (url: string) =>
console.log(
`${prefix.success} ${chalk.white("Server running on")} ${chalk.cyan.underline(url)}`,
),
banner: () =>
console.log(
`\n ${chalk.cyan.bold(AppNames.name)} ${chalk.gray(`v${AppNames.version}`)}\n`,
`\n ${chalk.cyan.bold(AppNames.name)} ${chalk.gray(`v${global.CURRENT_VERSION || AppNames["version"]}`)}\n`,
),
};