From 52dde6c0abaab9f637c17f9b4594812b53e55159 Mon Sep 17 00:00:00 2001 From: Benjamin Toby Date: Fri, 20 Mar 2026 11:19:22 +0100 Subject: [PATCH] Update HMR. Make it true HMR. Add URL to page server props --- dist/commands/start/index.js | 1 + dist/data/app-data.js | 2 + dist/functions/bundler/all-pages-bundler.js | 88 ++------- .../grab-artifacts-from-bundled-result.js | 35 ++++ .../bundler/grab-client-hydration-script.js | 65 +++++++ dist/functions/init.js | 6 + dist/functions/server/handle-files.js | 24 +++ dist/functions/server/handle-hmr-update.js | 54 ++++++ dist/functions/server/handle-hmr.js | 34 ++++ dist/functions/server/handle-public.js | 25 +++ dist/functions/server/server-params-gen.js | 101 +++-------- dist/functions/server/watcher.js | 9 +- .../server/web-pages/generate-web-html.js | 12 +- ...web-page-response-from-component-return.js | 55 ++++++ .../server/web-pages/grab-file-path-module.js | 23 +-- .../grab-page-bundled-react-component.js | 5 +- .../server/web-pages/grab-page-component.js | 46 ++--- .../server/web-pages/grab-root-file.js | 21 +++ .../web-pages/grab-tsx-string-module.js | 18 +- .../grab-web-page-hydration-script.js | 148 +++++++++++----- .../server/web-pages/handle-web-pages.js | 65 ++----- .../web-pages/tailwind-esbuild-plugin.js | 20 +++ .../server/web-pages/write-hmr-tsx-module.js | 106 +++++++++++ dist/index.js | 1 + dist/utils/log.js | 12 +- package.json | 3 +- src/commands/start/index.ts | 2 + src/data/app-data.ts | 2 + src/functions/bundler/all-pages-bundler.ts | 112 ++---------- .../grab-artifacts-from-bundled-result.ts | 56 ++++++ .../bundler/grab-client-hydration-script.ts | 84 +++++++++ src/functions/init.ts | 9 + src/functions/server/handle-files.ts | 33 ++++ src/functions/server/handle-hmr-update.ts | 84 +++++++++ src/functions/server/handle-hmr.ts | 50 ++++++ src/functions/server/handle-public.ts | 40 +++++ src/functions/server/server-params-gen.ts | 133 +++----------- src/functions/server/watcher.tsx | 10 +- .../server/web-pages/generate-web-html.tsx | 14 +- ...eb-page-response-from-component-return.tsx | 79 +++++++++ .../web-pages/grab-file-path-module.tsx | 29 +-- .../grab-page-bundled-react-component.tsx | 5 +- .../server/web-pages/grab-page-component.tsx | 55 +++--- .../server/web-pages/grab-root-file.tsx | 25 +++ .../web-pages/grab-tsx-string-module.tsx | 20 +-- .../grab-web-page-hydration-script.tsx | 167 ++++++++++++------ .../server/web-pages/handle-web-pages.tsx | 90 ++-------- .../web-pages/tailwind-esbuild-plugin.tsx | 23 +++ .../server/web-pages/write-hmr-tsx-module.tsx | 126 +++++++++++++ src/index.ts | 6 + src/types/index.ts | 3 + src/utils/log.ts | 20 +-- 52 files changed, 1548 insertions(+), 708 deletions(-) create mode 100644 dist/functions/bundler/grab-artifacts-from-bundled-result.js create mode 100644 dist/functions/bundler/grab-client-hydration-script.js create mode 100644 dist/functions/server/handle-files.js create mode 100644 dist/functions/server/handle-hmr-update.js create mode 100644 dist/functions/server/handle-hmr.js create mode 100644 dist/functions/server/handle-public.js create mode 100644 dist/functions/server/web-pages/generate-web-page-response-from-component-return.js create mode 100644 dist/functions/server/web-pages/grab-root-file.js create mode 100644 dist/functions/server/web-pages/tailwind-esbuild-plugin.js create mode 100644 dist/functions/server/web-pages/write-hmr-tsx-module.js create mode 100644 src/functions/bundler/grab-artifacts-from-bundled-result.ts create mode 100644 src/functions/bundler/grab-client-hydration-script.ts create mode 100644 src/functions/server/handle-files.ts create mode 100644 src/functions/server/handle-hmr-update.ts create mode 100644 src/functions/server/handle-hmr.ts create mode 100644 src/functions/server/handle-public.ts create mode 100644 src/functions/server/web-pages/generate-web-page-response-from-component-return.tsx create mode 100644 src/functions/server/web-pages/grab-root-file.tsx create mode 100644 src/functions/server/web-pages/tailwind-esbuild-plugin.tsx create mode 100644 src/functions/server/web-pages/write-hmr-tsx-module.tsx 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`, ), };