diff --git a/dist/commands/start/index.js b/dist/commands/start/index.js
index 8653aa7..4bfc9f6 100644
--- a/dist/commands/start/index.js
+++ b/dist/commands/start/index.js
@@ -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 };
diff --git a/dist/data/app-data.js b/dist/data/app-data.js
index cc37daf..6d31a38 100644
--- a/dist/data/app-data.js
+++ b/dist/data/app-data.js
@@ -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",
};
diff --git a/dist/functions/bundler/all-pages-bundler.js b/dist/functions/bundler/all-pages-bundler.js
index 9df48a7..349b1b0 100644
--- a/dist/functions/bundler/all-pages-bundler.js
+++ b/dist/functions/bundler/all-pages-bundler.js
@@ -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 = \n`;
- }
- else {
- txt += `const component = \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) {
diff --git a/dist/functions/bundler/grab-artifacts-from-bundled-result.js b/dist/functions/bundler/grab-artifacts-from-bundled-result.js
new file mode 100644
index 0000000..ae228a8
--- /dev/null
+++ b/dist/functions/bundler/grab-artifacts-from-bundled-result.js
@@ -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;
+}
diff --git a/dist/functions/bundler/grab-client-hydration-script.js b/dist/functions/bundler/grab-client-hydration-script.js
new file mode 100644
index 0000000..d214a5b
--- /dev/null
+++ b/dist/functions/bundler/grab-client-hydration-script.js
@@ -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 = \n`;
+ // } else {
+ // txt += `const component = \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 = \n`;
+ }
+ else {
+ txt += `const component = \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();\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();\n`;
+ // txt += `};\n`;
+ // } else {
+ // }
+ return txt;
+}
diff --git a/dist/functions/init.js b/dist/functions/init.js
index 2db331a..cd915c8 100644
--- a/dist/functions/init.js
+++ b/dist/functions/init.js
@@ -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];
diff --git a/dist/functions/server/handle-files.js b/dist/functions/server/handle-files.js
new file mode 100644
index 0000000..e578e42
--- /dev/null
+++ b/dist/functions/server/handle-files.js
@@ -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,
+ });
+ }
+}
diff --git a/dist/functions/server/handle-hmr-update.js b/dist/functions/server/handle-hmr-update.js
new file mode 100644
index 0000000..542ed08
--- /dev/null
+++ b/dist/functions/server/handle-hmr-update.js
@@ -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,
+ });
+ }
+}
diff --git a/dist/functions/server/handle-hmr.js b/dist/functions/server/handle-hmr.js
new file mode 100644
index 0000000..555097b
--- /dev/null
+++ b/dist/functions/server/handle-hmr.js
@@ -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",
+ },
+ });
+}
diff --git a/dist/functions/server/handle-public.js b/dist/functions/server/handle-public.js
new file mode 100644
index 0000000..1350519
--- /dev/null
+++ b/dist/functions/server/handle-public.js
@@ -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,
+ });
+ }
+}
diff --git a/dist/functions/server/server-params-gen.js b/dist/functions/server/server-params-gen.js
index f1739ee..2671b1a 100644
--- a/dist/functions/server/server-params-gen.js
+++ b/dist/functions/server/server-params-gen.js
@@ -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}`, {
diff --git a/dist/functions/server/watcher.js b/dist/functions/server/watcher.js
index 58172d8..a0803b9 100644
--- a/dist/functions/server/watcher.js
+++ b/dist/functions/server/watcher.js
@@ -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;
}
diff --git a/dist/functions/server/web-pages/generate-web-html.js b/dist/functions/server/web-pages/generate-web-html.js
index 3281742..adc3325 100644
--- a/dist/functions/server/web-pages/generate-web-html.js
+++ b/dist/functions/server/web-pages/generate-web-html.js
@@ -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 += ` \n`;
if (bundledMap?.path) {
- html += ` \n`;
+ html += ` \n`;
}
if (isDevelopment()) {
html += `\n`;
diff --git a/dist/functions/server/web-pages/generate-web-page-response-from-component-return.js b/dist/functions/server/web-pages/generate-web-page-response-from-component-return.js
new file mode 100644
index 0000000..a7ac2a3
--- /dev/null
+++ b/dist/functions/server/web-pages/generate-web-page-response-from-component-return.js
@@ -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;
+}
diff --git a/dist/functions/server/web-pages/grab-file-path-module.js b/dist/functions/server/web-pages/grab-file-path-module.js
index 557a60c..4a2acb8 100644
--- a/dist/functions/server/web-pages/grab-file-path-module.js
+++ b/dist/functions/server/web-pages/grab-file-path-module.js
@@ -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,
});
diff --git a/dist/functions/server/web-pages/grab-page-bundled-react-component.js b/dist/functions/server/web-pages/grab-page-bundled-react-component.js
index 8d53a0e..50c437d 100644
--- a/dist/functions/server/web-pages/grab-page-bundled-react-component.js
+++ b/dist/functions/server/web-pages/grab-page-bundled-react-component.js
@@ -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 += ` \n`;
+ tsx += ` \n`;
}
else {
- tsx += ` \n`;
+ tsx += ` \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) {
diff --git a/dist/functions/server/web-pages/grab-page-component.js b/dist/functions/server/web-pages/grab-page-component.js
index 9cb54f6..595a082 100644
--- a/dist/functions/server/web-pages/grab-page-component.js
+++ b/dist/functions/server/web-pages/grab-page-component.js
@@ -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,
diff --git a/dist/functions/server/web-pages/grab-root-file.js b/dist/functions/server/web-pages/grab-root-file.js
new file mode 100644
index 0000000..f54c253
--- /dev/null
+++ b/dist/functions/server/web-pages/grab-root-file.js
@@ -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 };
+}
diff --git a/dist/functions/server/web-pages/grab-tsx-string-module.js b/dist/functions/server/web-pages/grab-tsx-string-module.js
index 5b4b30b..3c62216 100644
--- a/dist/functions/server/web-pages/grab-tsx-string-module.js
+++ b/dist/functions/server/web-pages/grab-tsx-string-module.js
@@ -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,
diff --git a/dist/functions/server/web-pages/grab-web-page-hydration-script.js b/dist/functions/server/web-pages/grab-web-page-hydration-script.js
index 4f5a8d4..0ddd82f 100644
--- a/dist/functions/server/web-pages/grab-web-page-hydration-script.js
+++ b/dist/functions/server/web-pages/grab-web-page-hydration-script.js
@@ -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 = ;\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 = ;\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;
+// }
diff --git a/dist/functions/server/web-pages/handle-web-pages.js b/dist/functions/server/web-pages/handle-web-pages.js
index ef3e814..41c9da3 100644
--- a/dist/functions/server/web-pages/handle-web-pages.js
+++ b/dist/functions/server/web-pages/handle-web-pages.js
@@ -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;
}
diff --git a/dist/functions/server/web-pages/tailwind-esbuild-plugin.js b/dist/functions/server/web-pages/tailwind-esbuild-plugin.js
new file mode 100644
index 0000000..1826508
--- /dev/null
+++ b/dist/functions/server/web-pages/tailwind-esbuild-plugin.js
@@ -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;
diff --git a/dist/functions/server/web-pages/write-hmr-tsx-module.js b/dist/functions/server/web-pages/write-hmr-tsx-module.js
new file mode 100644
index 0000000..23a0344
--- /dev/null
+++ b/dist/functions/server/web-pages/write-hmr-tsx-module.js
@@ -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 = {
+// 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;
+// }
+// }
diff --git a/dist/index.js b/dist/index.js
index 61edf9f..3f80e77 100755
--- a/dist/index.js
+++ b/dist/index.js
@@ -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({
diff --git a/dist/utils/log.js b/dist/utils/log.js
index 2d53a26..450a4ac 100644
--- a/dist/utils/log.js
+++ b/dist/utils/log.js
@@ -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`),
};
diff --git a/package.json b/package.json
index 98fda44..4822060 100644
--- a/package.json
+++ b/package.json
@@ -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": {
diff --git a/src/commands/start/index.ts b/src/commands/start/index.ts
index 3053cf2..ad0c0e4 100644
--- a/src/commands/start/index.ts
+++ b/src/commands/start/index.ts
@@ -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();
diff --git a/src/data/app-data.ts b/src/data/app-data.ts
index b90357f..8e4bdd2 100644
--- a/src/data/app-data.ts
+++ b/src/data/app-data.ts
@@ -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;
diff --git a/src/functions/bundler/all-pages-bundler.ts b/src/functions/bundler/all-pages-bundler.ts
index 44d69c1..43e01bd 100644
--- a/src/functions/bundler/all-pages-bundler.ts
+++ b/src/functions/bundler/all-pages-bundler.ts
@@ -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 = {};
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 = \n`;
- } else {
- txt += `const component = \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();
diff --git a/src/functions/bundler/grab-artifacts-from-bundled-result.ts b/src/functions/bundler/grab-artifacts-from-bundled-result.ts
new file mode 100644
index 0000000..59face0
--- /dev/null
+++ b/src/functions/bundler/grab-artifacts-from-bundled-result.ts
@@ -0,0 +1,56 @@
+import path from "path";
+import * as esbuild from "esbuild";
+import type { BundlerCTXMap, PageFiles } from "../../types";
+
+type Params = {
+ result: esbuild.BuildResult;
+ 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;
+}
diff --git a/src/functions/bundler/grab-client-hydration-script.ts b/src/functions/bundler/grab-client-hydration-script.ts
new file mode 100644
index 0000000..d492717
--- /dev/null
+++ b/src/functions/bundler/grab-client-hydration-script.ts
@@ -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 = \n`;
+ // } else {
+ // txt += `const component = \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 = \n`;
+ } else {
+ txt += `const component = \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();\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();\n`;
+ // txt += `};\n`;
+ // } else {
+ // }
+
+ return txt;
+}
diff --git a/src/functions/init.ts b/src/functions/init.ts
index a4ad74f..c0acab9 100644
--- a/src/functions/init.ts
+++ b/src/functions/init.ts
@@ -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
>)[];
diff --git a/src/functions/server/handle-files.ts b/src/functions/server/handle-files.ts
new file mode 100644
index 0000000..e98232b
--- /dev/null
+++ b/src/functions/server/handle-files.ts
@@ -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 {
+ 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,
+ });
+ }
+}
diff --git a/src/functions/server/handle-hmr-update.ts b/src/functions/server/handle-hmr-update.ts
new file mode 100644
index 0000000..75fb3f9
--- /dev/null
+++ b/src/functions/server/handle-hmr-update.ts
@@ -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 {
+ 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,
+ });
+ }
+}
diff --git a/src/functions/server/handle-hmr.ts b/src/functions/server/handle-hmr.ts
new file mode 100644
index 0000000..3b00b3c
--- /dev/null
+++ b/src/functions/server/handle-hmr.ts
@@ -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 {
+ 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;
+ 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",
+ },
+ });
+}
diff --git a/src/functions/server/handle-public.ts b/src/functions/server/handle-public.ts
new file mode 100644
index 0000000..fb46f7d
--- /dev/null
+++ b/src/functions/server/handle-public.ts
@@ -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 {
+ 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,
+ });
+ }
+}
diff --git a/src/functions/server/server-params-gen.ts b/src/functions/server/server-params-gen.ts
index b4d68a5..b754d95 100644
--- a/src/functions/server/server-params-gen.ts
+++ b/src/functions/server/server-params-gen.ts
@@ -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 {
const port = grabAppPort();
- const { PUBLIC_DIR } = grabDirNames();
const is_dev = isDevelopment();
@@ -26,6 +26,8 @@ export default async function (params?: Params): Promise {
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 {
}
}
- 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;
- 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.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,
diff --git a/src/functions/server/watcher.tsx b/src/functions/server/watcher.tsx
index 64bcc39..85b7259 100644
--- a/src/functions/server/watcher.tsx
+++ b/src/functions/server/watcher.tsx
@@ -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;
}
diff --git a/src/functions/server/web-pages/generate-web-html.tsx b/src/functions/server/web-pages/generate-web-html.tsx
index 7317113..ee80103 100644
--- a/src/functions/server/web-pages/generate-web-html.tsx
+++ b/src/functions/server/web-pages/generate-web-html.tsx
@@ -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()
: "";
@@ -46,7 +58,7 @@ export default async function genWebHTML({
}\n`;
if (bundledMap?.path) {
- html += ` \n`;
+ html += ` \n`;
}
if (isDevelopment()) {
diff --git a/src/functions/server/web-pages/generate-web-page-response-from-component-return.tsx b/src/functions/server/web-pages/generate-web-page-response-from-component-return.tsx
new file mode 100644
index 0000000..cb810e4
--- /dev/null
+++ b/src/functions/server/web-pages/generate-web-page-response-from-component-return.tsx
@@ -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;
+}
diff --git a/src/functions/server/web-pages/grab-file-path-module.tsx b/src/functions/server/web-pages/grab-file-path-module.tsx
index db6e52e..0e2fe23 100644
--- a/src/functions/server/web-pages/grab-file-path-module.tsx
+++ b/src/functions/server/web-pages/grab-file-path-module.tsx
@@ -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({
file_path,
+ out_file,
}: Params): Promise {
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({
),
},
metafile: true,
- plugins: [tailwindPlugin],
+ plugins: [tailwindEsbuildPlugin],
jsx: "automatic",
outfile: target_cache_file_path,
});
diff --git a/src/functions/server/web-pages/grab-page-bundled-react-component.tsx b/src/functions/server/web-pages/grab-page-bundled-react-component.tsx
index ef2b5d8..dffb56b 100644
--- a/src/functions/server/web-pages/grab-page-bundled-react-component.tsx
+++ b/src/functions/server/web-pages/grab-page-bundled-react-component.tsx
@@ -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 += ` \n`;
+ tsx += ` \n`;
} else {
- tsx += ` \n`;
+ tsx += ` \n`;
}
tsx += ` )\n`;
tsx += `}\n`;
@@ -44,6 +44,7 @@ export default async function grabPageBundledReactComponent({
return {
component,
server_res,
+ tsx,
};
} catch (error: any) {
return undefined;
diff --git a/src/functions/server/web-pages/grab-page-component.tsx b/src/functions/server/web-pages/grab-page-component.tsx
index 48ba69b..53b34e9 100644
--- a/src/functions/server/web-pages/grab-page-component.tsx
+++ b/src/functions/server/web-pages/grab-page-component.tsx
@@ -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 {
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;
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,
diff --git a/src/functions/server/web-pages/grab-root-file.tsx b/src/functions/server/web-pages/grab-root-file.tsx
new file mode 100644
index 0000000..c4cda36
--- /dev/null
+++ b/src/functions/server/web-pages/grab-root-file.tsx
@@ -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 };
+}
diff --git a/src/functions/server/web-pages/grab-tsx-string-module.tsx b/src/functions/server/web-pages/grab-tsx-string-module.tsx
index ec95525..f0ec9f1 100644
--- a/src/functions/server/web-pages/grab-tsx-string-module.tsx
+++ b/src/functions/server/web-pages/grab-tsx-string-module.tsx
@@ -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({
tsx,
file_path,
@@ -63,7 +47,7 @@ export default async function grabTsxStringModule({
),
},
metafile: true,
- plugins: [tailwindPlugin],
+ plugins: [tailwindEsbuildPlugin],
jsx: "automatic",
write: true,
outfile: out_file_path,
diff --git a/src/functions/server/web-pages/grab-web-page-hydration-script.tsx b/src/functions/server/web-pages/grab-web-page-hydration-script.tsx
index 8eb04a1..46e258e 100644
--- a/src/functions/server/web-pages/grab-web-page-hydration-script.tsx
+++ b/src/functions/server/web-pages/grab-web-page-hydration-script.tsx
@@ -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 = ;\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 = ;\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;
+// }
diff --git a/src/functions/server/web-pages/handle-web-pages.tsx b/src/functions/server/web-pages/handle-web-pages.tsx
index cb48f26..263195d 100644
--- a/src/functions/server/web-pages/handle-web-pages.tsx
+++ b/src/functions/server/web-pages/handle-web-pages.tsx
@@ -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;
}
diff --git a/src/functions/server/web-pages/tailwind-esbuild-plugin.tsx b/src/functions/server/web-pages/tailwind-esbuild-plugin.tsx
new file mode 100644
index 0000000..9e9a8e3
--- /dev/null
+++ b/src/functions/server/web-pages/tailwind-esbuild-plugin.tsx
@@ -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;
diff --git a/src/functions/server/web-pages/write-hmr-tsx-module.tsx b/src/functions/server/web-pages/write-hmr-tsx-module.tsx
new file mode 100644
index 0000000..4a3bb94
--- /dev/null
+++ b/src/functions/server/web-pages/write-hmr-tsx-module.tsx
@@ -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
+ | 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 = {
+// 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;
+// }
+// }
diff --git a/src/index.ts b/src/index.ts
index 3bd6dce..f50456c 100755
--- a/src/index.ts
+++ b/src/index.ts
@@ -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();
diff --git a/src/types/index.ts b/src/types/index.ts
index 85c0807..439c3bc 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -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;
+ debug?: boolean;
};
export type GrabPageReactBundledComponentRes = {
component: JSX.Element;
server_res?: BunextPageModuleServerReturn;
+ tsx?: string;
};
export type PageFiles = {
diff --git a/src/utils/log.ts b/src/utils/log.ts
index 7fafec2..7a6bfe0 100644
--- a/src/utils/log.ts
+++ b/src/utils/log.ts
@@ -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`,
),
};