Update HMR. Make it true HMR. Add URL to page server props
This commit is contained in:
parent
7804a34951
commit
52dde6c0ab
1
dist/commands/start/index.js
vendored
1
dist/commands/start/index.js
vendored
@ -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 };
|
||||
|
||||
2
dist/data/app-data.js
vendored
2
dist/data/app-data.js
vendored
@ -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",
|
||||
};
|
||||
|
||||
88
dist/functions/bundler/all-pages-bundler.js
vendored
88
dist/functions/bundler/all-pages-bundler.js
vendored
@ -1,56 +1,23 @@
|
||||
import { existsSync, writeFileSync } from "fs";
|
||||
import path from "path";
|
||||
import { writeFileSync } from "fs";
|
||||
import * as esbuild from "esbuild";
|
||||
import postcss from "postcss";
|
||||
import tailwindcss from "@tailwindcss/postcss";
|
||||
import { readFile } from "fs/promises";
|
||||
import grabAllPages from "../../utils/grab-all-pages";
|
||||
import grabDirNames from "../../utils/grab-dir-names";
|
||||
import AppNames from "../../utils/grab-app-names";
|
||||
import isDevelopment from "../../utils/is-development";
|
||||
import { execSync } from "child_process";
|
||||
import grabConstants from "../../utils/grab-constants";
|
||||
import { log } from "../../utils/log";
|
||||
const { HYDRATION_DST_DIR, PAGES_DIR, HYDRATION_DST_DIR_MAP_JSON_FILE } = grabDirNames();
|
||||
const tailwindPlugin = {
|
||||
name: "tailwindcss",
|
||||
setup(build) {
|
||||
build.onLoad({ filter: /\.css$/ }, async (args) => {
|
||||
const source = await readFile(args.path, "utf-8");
|
||||
const result = await postcss([tailwindcss()]).process(source, {
|
||||
from: args.path,
|
||||
});
|
||||
return {
|
||||
contents: result.css,
|
||||
loader: "css",
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
import tailwindEsbuildPlugin from "../server/web-pages/tailwind-esbuild-plugin";
|
||||
import grabClientHydrationScript from "./grab-client-hydration-script";
|
||||
import grabArtifactsFromBundledResults from "./grab-artifacts-from-bundled-result";
|
||||
const { HYDRATION_DST_DIR, HYDRATION_DST_DIR_MAP_JSON_FILE } = grabDirNames();
|
||||
export default async function allPagesBundler(params) {
|
||||
const pages = grabAllPages({ exclude_api: true });
|
||||
const { ClientRootElementIDName, ClientRootComponentWindowName } = grabConstants();
|
||||
const virtualEntries = {};
|
||||
const dev = isDevelopment();
|
||||
const root_component_path = path.join(PAGES_DIR, `${AppNames["RootPagesComponentName"]}.tsx`);
|
||||
const does_root_exist = existsSync(root_component_path);
|
||||
for (const page of pages) {
|
||||
const key = page.local_path;
|
||||
let txt = ``;
|
||||
txt += `import { hydrateRoot } from "react-dom/client";\n`;
|
||||
if (does_root_exist) {
|
||||
txt += `import Root from "${root_component_path}";\n`;
|
||||
}
|
||||
txt += `import Page from "${page.local_path}";\n\n`;
|
||||
txt += `const pageProps = window.__PAGE_PROPS__ || {};\n`;
|
||||
if (does_root_exist) {
|
||||
txt += `const component = <Root {...pageProps}><Page {...pageProps} /></Root>\n`;
|
||||
}
|
||||
else {
|
||||
txt += `const component = <Page {...pageProps} />\n`;
|
||||
}
|
||||
txt += `const root = hydrateRoot(document.getElementById("${ClientRootElementIDName}"), component);\n\n`;
|
||||
txt += `window.${ClientRootComponentWindowName} = root;\n`;
|
||||
const txt = grabClientHydrationScript({
|
||||
page_local_path: page.local_path,
|
||||
});
|
||||
virtualEntries[key] = txt;
|
||||
}
|
||||
const virtualPlugin = {
|
||||
@ -77,38 +44,18 @@ export default async function allPagesBundler(params) {
|
||||
build.onEnd((result) => {
|
||||
if (result.errors.length > 0)
|
||||
return;
|
||||
const artifacts = Object.entries(result.metafile.outputs)
|
||||
.filter(([, meta]) => meta.entryPoint)
|
||||
.map(([outputPath, meta]) => {
|
||||
const target_page = pages.find((p) => {
|
||||
return (meta.entryPoint === `virtual:${p.local_path}`);
|
||||
});
|
||||
if (!target_page || !meta.entryPoint) {
|
||||
return undefined;
|
||||
}
|
||||
const { file_name, local_path, url_path } = target_page;
|
||||
const cssPath = meta.cssBundle || undefined;
|
||||
return {
|
||||
path: outputPath,
|
||||
hash: path.basename(outputPath, path.extname(outputPath)),
|
||||
type: outputPath.endsWith(".css")
|
||||
? "text/css"
|
||||
: "text/javascript",
|
||||
entrypoint: meta.entryPoint,
|
||||
css_path: cssPath,
|
||||
file_name,
|
||||
local_path,
|
||||
url_path,
|
||||
};
|
||||
const artifacts = grabArtifactsFromBundledResults({
|
||||
pages,
|
||||
result,
|
||||
});
|
||||
if (artifacts.length > 0) {
|
||||
const final_artifacts = artifacts.filter((a) => Boolean(a?.entrypoint));
|
||||
global.BUNDLER_CTX_MAP = final_artifacts;
|
||||
params?.post_build_fn?.({ artifacts: final_artifacts });
|
||||
if (artifacts?.[0] && artifacts.length > 0) {
|
||||
global.BUNDLER_CTX_MAP = artifacts;
|
||||
global.PAGE_FILES = pages;
|
||||
params?.post_build_fn?.({ artifacts });
|
||||
writeFileSync(HYDRATION_DST_DIR_MAP_JSON_FILE, JSON.stringify(artifacts));
|
||||
}
|
||||
const elapsed = (performance.now() - buildStart).toFixed(0);
|
||||
log.success(`Built in ${elapsed}ms`);
|
||||
log.success(`[Built] in ${elapsed}ms`);
|
||||
if (params?.exit_after_first_build) {
|
||||
process.exit();
|
||||
}
|
||||
@ -129,9 +76,10 @@ export default async function allPagesBundler(params) {
|
||||
},
|
||||
entryNames: "[dir]/[name]/[hash]",
|
||||
metafile: true,
|
||||
plugins: [tailwindPlugin, virtualPlugin, artifactTracker],
|
||||
plugins: [tailwindEsbuildPlugin, virtualPlugin, artifactTracker],
|
||||
jsx: "automatic",
|
||||
splitting: true,
|
||||
logLevel: "silent",
|
||||
});
|
||||
await ctx.rebuild();
|
||||
if (params?.watch) {
|
||||
|
||||
35
dist/functions/bundler/grab-artifacts-from-bundled-result.js
vendored
Normal file
35
dist/functions/bundler/grab-artifacts-from-bundled-result.js
vendored
Normal file
@ -0,0 +1,35 @@
|
||||
import path from "path";
|
||||
import * as esbuild from "esbuild";
|
||||
export default function grabArtifactsFromBundledResults({ result, pages, }) {
|
||||
if (result.errors.length > 0)
|
||||
return;
|
||||
const artifacts = Object.entries(result.metafile.outputs)
|
||||
.filter(([, meta]) => meta.entryPoint)
|
||||
.map(([outputPath, meta]) => {
|
||||
const target_page = pages.find((p) => {
|
||||
return meta.entryPoint === `virtual:${p.local_path}`;
|
||||
});
|
||||
if (!target_page || !meta.entryPoint) {
|
||||
return undefined;
|
||||
}
|
||||
const { file_name, local_path, url_path } = target_page;
|
||||
const cssPath = meta.cssBundle || undefined;
|
||||
return {
|
||||
path: outputPath,
|
||||
hash: path.basename(outputPath, path.extname(outputPath)),
|
||||
type: outputPath.endsWith(".css")
|
||||
? "text/css"
|
||||
: "text/javascript",
|
||||
entrypoint: meta.entryPoint,
|
||||
css_path: cssPath,
|
||||
file_name,
|
||||
local_path,
|
||||
url_path,
|
||||
};
|
||||
});
|
||||
if (artifacts.length > 0) {
|
||||
const final_artifacts = artifacts.filter((a) => Boolean(a?.entrypoint));
|
||||
return final_artifacts;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
65
dist/functions/bundler/grab-client-hydration-script.js
vendored
Normal file
65
dist/functions/bundler/grab-client-hydration-script.js
vendored
Normal file
@ -0,0 +1,65 @@
|
||||
import { existsSync } from "fs";
|
||||
import path from "path";
|
||||
import grabDirNames from "../../utils/grab-dir-names";
|
||||
import AppNames from "../../utils/grab-app-names";
|
||||
import grabConstants from "../../utils/grab-constants";
|
||||
const { PAGES_DIR } = grabDirNames();
|
||||
export default function grabClientHydrationScript({ page_local_path }) {
|
||||
const { ClientRootElementIDName, ClientRootComponentWindowName } = grabConstants();
|
||||
const root_component_path = path.join(PAGES_DIR, `${AppNames["RootPagesComponentName"]}.tsx`);
|
||||
const does_root_exist = existsSync(root_component_path);
|
||||
// let txt = ``;
|
||||
// txt += `import { hydrateRoot } from "react-dom/client";\n`;
|
||||
// if (does_root_exist) {
|
||||
// txt += `import Root from "${root_component_path}";\n`;
|
||||
// }
|
||||
// txt += `import Page from "${page.local_path}";\n\n`;
|
||||
// txt += `const pageProps = window.__PAGE_PROPS__ || {};\n`;
|
||||
// if (does_root_exist) {
|
||||
// txt += `const component = <Root {...pageProps}><Page {...pageProps} /></Root>\n`;
|
||||
// } else {
|
||||
// txt += `const component = <Page {...pageProps} />\n`;
|
||||
// }
|
||||
// txt += `const root = hydrateRoot(document.getElementById("${ClientRootElementIDName}"), component);\n\n`;
|
||||
// txt += `window.${ClientRootComponentWindowName} = root;\n`;
|
||||
let txt = ``;
|
||||
// txt += `import * as React from "react";\n`;
|
||||
// txt += `import * as ReactDOM from "react-dom";\n`;
|
||||
// txt += `import * as ReactDOMClient from "react-dom/client";\n`;
|
||||
// txt += `import * as JSXRuntime from "react/jsx-runtime";\n`;
|
||||
txt += `import { hydrateRoot, createElement } from "react-dom/client";\n`;
|
||||
if (does_root_exist) {
|
||||
txt += `import Root from "${root_component_path}";\n`;
|
||||
}
|
||||
txt += `import Page from "${page_local_path}";\n\n`;
|
||||
// txt += `window.__REACT__ = React;\n`;
|
||||
// txt += `window.__REACT_DOM__ = ReactDOM;\n`;
|
||||
// txt += `window.__REACT_DOM_CLIENT__ = ReactDOMClient;\n`;
|
||||
// txt += `window.__JSX_RUNTIME__ = JSXRuntime;\n\n`;
|
||||
txt += `const pageProps = window.__PAGE_PROPS__ || {};\n`;
|
||||
if (does_root_exist) {
|
||||
txt += `const component = <Root {...pageProps}><Page {...pageProps} /></Root>\n`;
|
||||
}
|
||||
else {
|
||||
txt += `const component = <Page {...pageProps} />\n`;
|
||||
}
|
||||
txt += `if (window.${ClientRootComponentWindowName}?.render) {\n`;
|
||||
txt += ` window.${ClientRootComponentWindowName}.render(component);\n`;
|
||||
txt += `} else {\n`;
|
||||
txt += ` const root = hydrateRoot(document.getElementById("${ClientRootElementIDName}"), component);\n\n`;
|
||||
txt += ` window.${ClientRootComponentWindowName} = root;\n`;
|
||||
txt += ` window.__BUNEXT_RERENDER__ = (NewPage) => {\n`;
|
||||
txt += ` const props = window.__PAGE_PROPS__ || {};\n`;
|
||||
txt += ` root.render(<NewPage {...props} />);\n`;
|
||||
txt += ` };\n`;
|
||||
txt += `}\n`;
|
||||
// // HMR re-render helper
|
||||
// if (does_root_exist) {
|
||||
// txt += `window.__BUNEXT_RERENDER__ = (NewPage) => {\n`;
|
||||
// txt += ` const props = window.__PAGE_PROPS__ || {};\n`;
|
||||
// txt += ` root.render(<Root {...props}><NewPage {...props} /></Root>);\n`;
|
||||
// txt += `};\n`;
|
||||
// } else {
|
||||
// }
|
||||
return txt;
|
||||
}
|
||||
6
dist/functions/init.js
vendored
6
dist/functions/init.js
vendored
@ -1,10 +1,16 @@
|
||||
import { existsSync, mkdirSync, statSync, writeFileSync } from "fs";
|
||||
import grabDirNames from "../utils/grab-dir-names";
|
||||
import { execSync } from "child_process";
|
||||
import path from "path";
|
||||
export default async function () {
|
||||
const dirNames = grabDirNames();
|
||||
execSync(`rm -rf ${dirNames.BUNEXT_CACHE_DIR}`);
|
||||
execSync(`rm -rf ${dirNames.BUNX_CWD_MODULE_CACHE_DIR}`);
|
||||
try {
|
||||
const current_version = (await Bun.file(path.resolve(__dirname, "../../package.json")).json()).version;
|
||||
global.CURRENT_VERSION = current_version;
|
||||
}
|
||||
catch (error) { }
|
||||
const keys = Object.keys(dirNames);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i];
|
||||
|
||||
24
dist/functions/server/handle-files.js
vendored
Normal file
24
dist/functions/server/handle-files.js
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
import grabDirNames from "../../utils/grab-dir-names";
|
||||
import path from "path";
|
||||
import isDevelopment from "../../utils/is-development";
|
||||
import { existsSync } from "fs";
|
||||
const { PUBLIC_DIR } = grabDirNames();
|
||||
export default async function ({ req, server }) {
|
||||
try {
|
||||
const is_dev = isDevelopment();
|
||||
const url = new URL(req.url);
|
||||
const file_path = path.join(PUBLIC_DIR, url.pathname);
|
||||
if (!existsSync(file_path)) {
|
||||
return new Response(`File Doesn't Exist`, {
|
||||
status: 404,
|
||||
});
|
||||
}
|
||||
const file = Bun.file(file_path);
|
||||
return new Response(file);
|
||||
}
|
||||
catch (error) {
|
||||
return new Response(`File Not Found`, {
|
||||
status: 404,
|
||||
});
|
||||
}
|
||||
}
|
||||
54
dist/functions/server/handle-hmr-update.js
vendored
Normal file
54
dist/functions/server/handle-hmr-update.js
vendored
Normal file
@ -0,0 +1,54 @@
|
||||
import grabDirNames from "../../utils/grab-dir-names";
|
||||
import { AppData } from "../../data/app-data";
|
||||
import path from "path";
|
||||
import grabRootFile from "./web-pages/grab-root-file";
|
||||
import grabPageBundledReactComponent from "./web-pages/grab-page-bundled-react-component";
|
||||
import writeHMRTsxModule from "./web-pages/write-hmr-tsx-module";
|
||||
const { PUBLIC_DIR, BUNX_HYDRATION_SRC_DIR } = grabDirNames();
|
||||
export default async function ({ req, server }) {
|
||||
try {
|
||||
const url = new URL(req.url);
|
||||
const target_href = url.searchParams.get("href");
|
||||
if (!target_href) {
|
||||
return new Response(`No HREF passed to /${AppData["ClientHMRPath"]}`, { status: 404 });
|
||||
}
|
||||
const target_href_url = new URL(target_href);
|
||||
const match = global.ROUTER.match(target_href_url.pathname);
|
||||
if (!match?.filePath) {
|
||||
return new Response(`No pages file matched for this path`, {
|
||||
status: 404,
|
||||
});
|
||||
}
|
||||
const out_file = path.join(BUNX_HYDRATION_SRC_DIR, target_href_url.pathname, "index.js");
|
||||
const { root_file } = grabRootFile();
|
||||
const { tsx } = (await grabPageBundledReactComponent({
|
||||
file_path: match.filePath,
|
||||
root_file,
|
||||
})) || {};
|
||||
if (!tsx) {
|
||||
throw new Error(`Couldn't grab txt string`);
|
||||
}
|
||||
const artifact = await writeHMRTsxModule({
|
||||
tsx,
|
||||
out_file,
|
||||
});
|
||||
const file = Bun.file(out_file);
|
||||
if (await file.exists()) {
|
||||
return new Response(file, {
|
||||
headers: {
|
||||
"Content-Type": "text/javascript",
|
||||
},
|
||||
});
|
||||
}
|
||||
return new Response("Not found", {
|
||||
status: 404,
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
const error_msg = error.message;
|
||||
console.error(error_msg);
|
||||
return new Response(error_msg || "HMR Error", {
|
||||
status: 404,
|
||||
});
|
||||
}
|
||||
}
|
||||
34
dist/functions/server/handle-hmr.js
vendored
Normal file
34
dist/functions/server/handle-hmr.js
vendored
Normal file
@ -0,0 +1,34 @@
|
||||
import grabRouteParams from "../../utils/grab-route-params";
|
||||
import grabConstants from "../../utils/grab-constants";
|
||||
import grabRouter from "../../utils/grab-router";
|
||||
export default async function ({ req, server }) {
|
||||
const referer_url = new URL(req.headers.get("referer") || "");
|
||||
const match = global.ROUTER.match(referer_url.pathname);
|
||||
const target_map = match?.filePath
|
||||
? global.BUNDLER_CTX_MAP?.find((m) => m.local_path == match.filePath)
|
||||
: undefined;
|
||||
let controller;
|
||||
const stream = new ReadableStream({
|
||||
start(c) {
|
||||
controller = c;
|
||||
global.HMR_CONTROLLERS.push({
|
||||
controller: c,
|
||||
page_url: referer_url.href,
|
||||
target_map,
|
||||
});
|
||||
},
|
||||
cancel() {
|
||||
const targetControllerIndex = global.HMR_CONTROLLERS.findIndex((c) => c.controller == controller);
|
||||
if (typeof targetControllerIndex == "number" &&
|
||||
targetControllerIndex >= 0) {
|
||||
global.HMR_CONTROLLERS.splice(targetControllerIndex, 1);
|
||||
}
|
||||
},
|
||||
});
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
"Content-Type": "text/event-stream",
|
||||
Connection: "keep-alive",
|
||||
},
|
||||
});
|
||||
}
|
||||
25
dist/functions/server/handle-public.js
vendored
Normal file
25
dist/functions/server/handle-public.js
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
import grabDirNames from "../../utils/grab-dir-names";
|
||||
import path from "path";
|
||||
import isDevelopment from "../../utils/is-development";
|
||||
import { existsSync } from "fs";
|
||||
const { PUBLIC_DIR, BUNX_HYDRATION_SRC_DIR } = grabDirNames();
|
||||
export default async function ({ req, server }) {
|
||||
try {
|
||||
const is_dev = isDevelopment();
|
||||
const url = new URL(req.url);
|
||||
const file_path = path.join(PUBLIC_DIR, url.pathname.replace(/^\/public/, ""));
|
||||
if (!existsSync(file_path)) {
|
||||
return new Response(`Public File Doesn't Exist`, {
|
||||
status: 404,
|
||||
});
|
||||
}
|
||||
const file = Bun.file(file_path);
|
||||
let res_opts = {};
|
||||
return new Response(file, res_opts);
|
||||
}
|
||||
catch (error) {
|
||||
return new Response(`Public File Not Found`, {
|
||||
status: 404,
|
||||
});
|
||||
}
|
||||
}
|
||||
101
dist/functions/server/server-params-gen.js
vendored
101
dist/functions/server/server-params-gen.js
vendored
@ -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}`, {
|
||||
|
||||
9
dist/functions/server/watcher.js
vendored
9
dist/functions/server/watcher.js
vendored
@ -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;
|
||||
}
|
||||
|
||||
@ -5,10 +5,18 @@ import EJSON from "../../../utils/ejson";
|
||||
import isDevelopment from "../../../utils/is-development";
|
||||
import grabWebPageHydrationScript from "./grab-web-page-hydration-script";
|
||||
import grabWebMetaHTML from "./grab-web-meta-html";
|
||||
export default async function genWebHTML({ component, pageProps, bundledMap, head: Head, module, meta, routeParams, }) {
|
||||
import { log } from "../../../utils/log";
|
||||
import { AppData } from "../../../data/app-data";
|
||||
export default async function genWebHTML({ component, pageProps, bundledMap, head: Head, module, meta, routeParams, debug, }) {
|
||||
const { ClientRootElementIDName, ClientWindowPagePropsName } = grabContants();
|
||||
const { renderToString } = await import(path.join(process.cwd(), "node_modules", "react-dom", "server"));
|
||||
if (debug) {
|
||||
log.info("component", component);
|
||||
}
|
||||
const componentHTML = renderToString(component);
|
||||
if (debug) {
|
||||
log.info("componentHTML", componentHTML);
|
||||
}
|
||||
const headHTML = Head
|
||||
? renderToString(_jsx(Head, { serverRes: pageProps, ctx: routeParams }))
|
||||
: "";
|
||||
@ -25,7 +33,7 @@ export default async function genWebHTML({ component, pageProps, bundledMap, hea
|
||||
}
|
||||
html += ` <script>window.${ClientWindowPagePropsName} = ${EJSON.stringify(pageProps || {}) || "{}"}</script>\n`;
|
||||
if (bundledMap?.path) {
|
||||
html += ` <script src="/${bundledMap.path}" type="module" async></script>\n`;
|
||||
html += ` <script src="/${bundledMap.path}" type="module" id="${AppData["BunextClientHydrationScriptID"]}" async></script>\n`;
|
||||
}
|
||||
if (isDevelopment()) {
|
||||
html += `<script defer>\n${await grabWebPageHydrationScript({ bundledMap })}\n</script>\n`;
|
||||
|
||||
55
dist/functions/server/web-pages/generate-web-page-response-from-component-return.js
vendored
Normal file
55
dist/functions/server/web-pages/generate-web-page-response-from-component-return.js
vendored
Normal file
@ -0,0 +1,55 @@
|
||||
import isDevelopment from "../../../utils/is-development";
|
||||
import { log } from "../../../utils/log";
|
||||
import writeCache from "../../cache/write-cache";
|
||||
import genWebHTML from "./generate-web-html";
|
||||
export default async function generateWebPageResponseFromComponentReturn({ component, module, bundledMap, head, meta, routeParams, serverRes, debug, }) {
|
||||
const html = await genWebHTML({
|
||||
component,
|
||||
pageProps: serverRes,
|
||||
bundledMap,
|
||||
module,
|
||||
meta,
|
||||
head,
|
||||
routeParams,
|
||||
debug,
|
||||
});
|
||||
if (debug) {
|
||||
log.info("html", html);
|
||||
}
|
||||
if (serverRes?.redirect?.destination) {
|
||||
return Response.redirect(serverRes.redirect.destination, serverRes.redirect.permanent
|
||||
? 301
|
||||
: serverRes.redirect.status_code || 302);
|
||||
}
|
||||
const res_opts = {
|
||||
...serverRes?.responseOptions,
|
||||
headers: {
|
||||
"Content-Type": "text/html",
|
||||
...serverRes?.responseOptions?.headers,
|
||||
},
|
||||
};
|
||||
if (isDevelopment()) {
|
||||
res_opts.headers = {
|
||||
...res_opts.headers,
|
||||
"Cache-Control": "no-cache, no-store, must-revalidate",
|
||||
Pragma: "no-cache",
|
||||
Expires: "0",
|
||||
};
|
||||
}
|
||||
const cache_page = module.config?.cachePage || serverRes?.cachePage || false;
|
||||
const expiry_seconds = module.config?.cacheExpiry || serverRes?.cacheExpiry;
|
||||
if (cache_page && routeParams?.url) {
|
||||
const key = routeParams.url.pathname + (routeParams.url.search || "");
|
||||
writeCache({
|
||||
key,
|
||||
value: html,
|
||||
paradigm: "html",
|
||||
expiry_seconds,
|
||||
});
|
||||
}
|
||||
const res = new Response(html, res_opts);
|
||||
if (routeParams?.resTransform) {
|
||||
return await routeParams.resTransform(res);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
@ -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,
|
||||
});
|
||||
|
||||
@ -13,10 +13,10 @@ export default async function grabPageBundledReactComponent({ file_path, root_fi
|
||||
tsx += `const props = JSON.parse("${server_res_json}")\n\n`;
|
||||
tsx += ` return (\n`;
|
||||
if (root_file) {
|
||||
tsx += ` <Root {...props}><Page {...props} /></Root>\n`;
|
||||
tsx += ` <Root suppressHydrationWarning={true} {...props}><Page {...props} /></Root>\n`;
|
||||
}
|
||||
else {
|
||||
tsx += ` <Page {...props} />\n`;
|
||||
tsx += ` <Page suppressHydrationWarning={true} {...props} />\n`;
|
||||
}
|
||||
tsx += ` )\n`;
|
||||
tsx += `}\n`;
|
||||
@ -26,6 +26,7 @@ export default async function grabPageBundledReactComponent({ file_path, root_fi
|
||||
return {
|
||||
component,
|
||||
server_res,
|
||||
tsx,
|
||||
};
|
||||
}
|
||||
catch (error) {
|
||||
|
||||
@ -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,
|
||||
|
||||
21
dist/functions/server/web-pages/grab-root-file.js
vendored
Normal file
21
dist/functions/server/web-pages/grab-root-file.js
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
import grabDirNames from "../../../utils/grab-dir-names";
|
||||
import path from "path";
|
||||
import AppNames from "../../../utils/grab-app-names";
|
||||
import { existsSync } from "fs";
|
||||
export default function grabRootFile() {
|
||||
const { PAGES_DIR } = grabDirNames();
|
||||
const root_pages_component_ts_file = `${path.join(PAGES_DIR, AppNames["RootPagesComponentName"])}.ts`;
|
||||
const root_pages_component_tsx_file = `${path.join(PAGES_DIR, AppNames["RootPagesComponentName"])}.tsx`;
|
||||
const root_pages_component_js_file = `${path.join(PAGES_DIR, AppNames["RootPagesComponentName"])}.js`;
|
||||
const root_pages_component_jsx_file = `${path.join(PAGES_DIR, AppNames["RootPagesComponentName"])}.jsx`;
|
||||
const root_file = existsSync(root_pages_component_tsx_file)
|
||||
? root_pages_component_tsx_file
|
||||
: existsSync(root_pages_component_ts_file)
|
||||
? root_pages_component_ts_file
|
||||
: existsSync(root_pages_component_jsx_file)
|
||||
? root_pages_component_jsx_file
|
||||
: existsSync(root_pages_component_js_file)
|
||||
? root_pages_component_js_file
|
||||
: undefined;
|
||||
return { root_file };
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -1,55 +1,109 @@
|
||||
import grabDirNames from "../../../utils/grab-dir-names";
|
||||
const { BUNX_HYDRATION_SRC_DIR } = grabDirNames();
|
||||
import { AppData } from "../../../data/app-data";
|
||||
export default async function ({ bundledMap }) {
|
||||
let script = "";
|
||||
// script += `import React from "react";\n`;
|
||||
// script += `import { hydrateRoot } from "react-dom/client";\n`;
|
||||
// script += `import App from "${page_file}";\n`;
|
||||
// script += `declare global {\n`;
|
||||
// script += ` interface Window {\n`;
|
||||
// script += ` ${ClientWindowPagePropsName}: any;\n`;
|
||||
// script += ` }\n`;
|
||||
// script += `}\n`;
|
||||
// script += `let root: any = null;\n\n`;
|
||||
// script += `const component = <App {...window.${ClientWindowPagePropsName}} />;\n\n`;
|
||||
// script += `const container = document.getElementById("${ClientRootElementIDName}");\n\n`;
|
||||
// script += `if (container) {\n`;
|
||||
// script += ` root = hydrateRoot(container, component);\n`;
|
||||
// script += `}\n\n`;
|
||||
script += `console.log(\`Development Environment\`);\n`;
|
||||
// script += `console.log(import.meta);\n`;
|
||||
// script += `if (import.meta.hot) {\n`;
|
||||
// script += ` console.log(\`HMR active\`);\n`;
|
||||
// script += ` import.meta.hot.dispose(() => {\n`;
|
||||
// script += ` console.log("dispose");\n`;
|
||||
// script += ` });\n`;
|
||||
// script += `}\n`;
|
||||
script += `console.log(\`Development Environment\`);\n\n`;
|
||||
script += `const hmr = new EventSource("/__hmr");\n`;
|
||||
script += `hmr.addEventListener("update", async (event) => {\n`;
|
||||
// script += ` console.log(\`HMR even received:\`, event);\n`;
|
||||
script += ` if (event.data) {\n`;
|
||||
script += ` console.log(\`HMR Changes Detected. Reloading ...\`);\n`;
|
||||
// script += ` console.log("event", event);\n`;
|
||||
// script += ` console.log("window.${ClientRootComponentWindowName}", window.${ClientRootComponentWindowName});\n\n`;
|
||||
// script += ` const event_data = JSON.parse(event.data);\n\n`;
|
||||
// script += ` const new_js_path = \`/\${event_data.target_map.path}\`;\n\n`;
|
||||
// script += ` console.log("event_data", event_data);\n\n`;
|
||||
// script += ` console.log("new_js_path", new_js_path);\n\n`;
|
||||
// script += ` if (window.${ClientRootComponentWindowName}) {\n`;
|
||||
// script += ` const new_component = await import(new_js_path);\n`;
|
||||
// script += ` window.${ClientRootComponentWindowName}.render(new_component);\n`;
|
||||
// script += ` }\n`;
|
||||
// script += ` import("${page_file}?t=" + event.data.update).then((module) => {\n`;
|
||||
// script += ` root.render(module.default);\n`;
|
||||
// script += ` })\n`;
|
||||
// script += ` console.log("root", root);\n`;
|
||||
// script += ` root.unmount();\n`;
|
||||
// script += ` const container = document.getElementById("${ClientRootElementIDName}");\n\n`;
|
||||
// script += ` root = hydrateRoot(container!, component);\n`;
|
||||
// script += ` window.history.pushState({ page: 1 }, "New Page Title", \`\${window.location.pathname}?v=\${Date.now()}\`);\n`;
|
||||
// script += ` root.render(component);\n`;
|
||||
script += ` window.location.reload();\n`;
|
||||
script += ` console.log(\`HMR Changes Detected. Updating ...\`);\n`;
|
||||
script += ` try {\n`;
|
||||
script += ` const data = JSON.parse(event.data);\n`;
|
||||
// script += ` console.log("data", data);\n`;
|
||||
// script += ` const modulePath = \`/\${data.target_map.path}\`;\n\n`;
|
||||
// script += ` const modulePath = \`/${AppData["ClientHMRPath"]}?href=\${window.location.href}&t=\${Date.now()}\`;\n\n`;
|
||||
// script += ` console.log("Fetching updated module ...", modulePath);\n\n`;
|
||||
// script += ` const newModule = await import(modulePath);\n\n`;
|
||||
// script += ` console.log("newModule", newModule);\n\n`;
|
||||
// script += ` if (window.__BUNEXT_RERENDER__ && newModule.default) {\n`;
|
||||
// script += ` window.__BUNEXT_RERENDER__(newModule.default);\n`;
|
||||
// script += ` console.log(\`HMR: Component updated in-place\`);\n`;
|
||||
// script += ` } else {\n`;
|
||||
// script += ` console.warn(\`HMR: No re-render helper found, falling back to reload\`);\n`;
|
||||
// // script += ` window.location.reload();\n`;
|
||||
// script += ` }\n\n`;
|
||||
script += ` if (data.target_map.css_path) {\n`;
|
||||
script += ` const oldLink = document.querySelector('link[rel="stylesheet"]');\n`;
|
||||
script += ` const newLink = document.createElement("link");\n`;
|
||||
script += ` newLink.rel = "stylesheet";\n`;
|
||||
script += ` newLink.href = \`/\${data.target_map.css_path}?t=\${Date.now()}\`;\n`;
|
||||
script += ` newLink.onload = () => oldLink?.remove();\n`;
|
||||
script += ` document.head.appendChild(newLink);\n`;
|
||||
script += ` }\n`;
|
||||
script += ` const newScriptPath = \`/\${data.target_map.path}?t=\${Date.now()}\`;\n\n`;
|
||||
script += ` const oldScript = document.getElementById("${AppData["BunextClientHydrationScriptID"]}");\n`;
|
||||
script += ` if (oldScript) {\n`;
|
||||
script += ` oldScript.remove();\n`;
|
||||
script += ` }\n\n`;
|
||||
script += ` const newScript = document.createElement("script");\n`;
|
||||
script += ` newScript.id = "${AppData["BunextClientHydrationScriptID"]}";\n`;
|
||||
script += ` newScript.type = "module";\n`;
|
||||
script += ` newScript.src = newScriptPath;\n`;
|
||||
// script += ` console.log("newScript", newScript);\n`;
|
||||
script += ` document.head.appendChild(newScript);\n\n`;
|
||||
script += ` } catch (err) {\n`;
|
||||
script += ` console.error("HMR update failed, falling back to reload:", err.message);\n`;
|
||||
// script += ` window.location.reload();\n`;
|
||||
script += ` }\n`;
|
||||
script += ` }\n`;
|
||||
script += ` });\n`;
|
||||
script += `});\n`;
|
||||
return script;
|
||||
}
|
||||
// import grabDirNames from "../../../utils/grab-dir-names";
|
||||
// import type { BundlerCTXMap, PageDistGenParams } from "../../../types";
|
||||
// const { BUNX_HYDRATION_SRC_DIR } = grabDirNames();
|
||||
// type Params = {
|
||||
// bundledMap?: BundlerCTXMap;
|
||||
// };
|
||||
// export default async function ({ bundledMap }: Params) {
|
||||
// let script = "";
|
||||
// // script += `import React from "react";\n`;
|
||||
// // script += `import { hydrateRoot } from "react-dom/client";\n`;
|
||||
// // script += `import App from "${page_file}";\n`;
|
||||
// // script += `declare global {\n`;
|
||||
// // script += ` interface Window {\n`;
|
||||
// // script += ` ${ClientWindowPagePropsName}: any;\n`;
|
||||
// // script += ` }\n`;
|
||||
// // script += `}\n`;
|
||||
// // script += `let root: any = null;\n\n`;
|
||||
// // script += `const component = <App {...window.${ClientWindowPagePropsName}} />;\n\n`;
|
||||
// // script += `const container = document.getElementById("${ClientRootElementIDName}");\n\n`;
|
||||
// // script += `if (container) {\n`;
|
||||
// // script += ` root = hydrateRoot(container, component);\n`;
|
||||
// // script += `}\n\n`;
|
||||
// script += `console.log(\`Development Environment\`);\n`;
|
||||
// // script += `console.log(import.meta);\n`;
|
||||
// // script += `if (import.meta.hot) {\n`;
|
||||
// // script += ` console.log(\`HMR active\`);\n`;
|
||||
// // script += ` import.meta.hot.dispose(() => {\n`;
|
||||
// // script += ` console.log("dispose");\n`;
|
||||
// // script += ` });\n`;
|
||||
// // script += `}\n`;
|
||||
// script += `const hmr = new EventSource("/__hmr");\n`;
|
||||
// script += `hmr.addEventListener("update", async (event) => {\n`;
|
||||
// // script += ` console.log(\`HMR even received:\`, event);\n`;
|
||||
// script += ` if (event.data) {\n`;
|
||||
// script += ` console.log(\`HMR Changes Detected. Reloading ...\`);\n`;
|
||||
// // script += ` console.log("event", event);\n`;
|
||||
// // script += ` console.log("window.${ClientRootComponentWindowName}", window.${ClientRootComponentWindowName});\n\n`;
|
||||
// // script += ` const event_data = JSON.parse(event.data);\n\n`;
|
||||
// // script += ` const new_js_path = \`/\${event_data.target_map.path}\`;\n\n`;
|
||||
// // script += ` console.log("event_data", event_data);\n\n`;
|
||||
// // script += ` console.log("new_js_path", new_js_path);\n\n`;
|
||||
// // script += ` if (window.${ClientRootComponentWindowName}) {\n`;
|
||||
// // script += ` const new_component = await import(new_js_path);\n`;
|
||||
// // script += ` window.${ClientRootComponentWindowName}.render(new_component);\n`;
|
||||
// // script += ` }\n`;
|
||||
// // script += ` import("${page_file}?t=" + event.data.update).then((module) => {\n`;
|
||||
// // script += ` root.render(module.default);\n`;
|
||||
// // script += ` })\n`;
|
||||
// // script += ` console.log("root", root);\n`;
|
||||
// // script += ` root.unmount();\n`;
|
||||
// // script += ` const container = document.getElementById("${ClientRootElementIDName}");\n\n`;
|
||||
// // script += ` root = hydrateRoot(container!, component);\n`;
|
||||
// // script += ` window.history.pushState({ page: 1 }, "New Page Title", \`\${window.location.pathname}?v=\${Date.now()}\`);\n`;
|
||||
// // script += ` root.render(component);\n`;
|
||||
// script += ` window.location.reload();\n`;
|
||||
// script += ` }\n`;
|
||||
// script += ` });\n`;
|
||||
// return script;
|
||||
// }
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
20
dist/functions/server/web-pages/tailwind-esbuild-plugin.js
vendored
Normal file
20
dist/functions/server/web-pages/tailwind-esbuild-plugin.js
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
import * as esbuild from "esbuild";
|
||||
import postcss from "postcss";
|
||||
import tailwindcss from "@tailwindcss/postcss";
|
||||
import { readFile } from "fs/promises";
|
||||
const tailwindEsbuildPlugin = {
|
||||
name: "tailwindcss",
|
||||
setup(build) {
|
||||
build.onLoad({ filter: /\.css$/ }, async (args) => {
|
||||
const source = await readFile(args.path, "utf-8");
|
||||
const result = await postcss([tailwindcss()]).process(source, {
|
||||
from: args.path,
|
||||
});
|
||||
return {
|
||||
contents: result.css,
|
||||
loader: "css",
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
export default tailwindEsbuildPlugin;
|
||||
106
dist/functions/server/web-pages/write-hmr-tsx-module.js
vendored
Normal file
106
dist/functions/server/web-pages/write-hmr-tsx-module.js
vendored
Normal file
@ -0,0 +1,106 @@
|
||||
import * as esbuild from "esbuild";
|
||||
import tailwindEsbuildPlugin from "./tailwind-esbuild-plugin";
|
||||
import path from "path";
|
||||
export default async function writeHMRTsxModule({ tsx, out_file }) {
|
||||
try {
|
||||
const build = await esbuild.build({
|
||||
stdin: {
|
||||
contents: tsx,
|
||||
resolveDir: process.cwd(),
|
||||
loader: "tsx",
|
||||
},
|
||||
bundle: true,
|
||||
format: "esm",
|
||||
target: "es2020",
|
||||
platform: "browser",
|
||||
external: [
|
||||
"react",
|
||||
"react-dom",
|
||||
"react/jsx-runtime",
|
||||
"react-dom/client",
|
||||
],
|
||||
minify: true,
|
||||
jsx: "automatic",
|
||||
outfile: out_file,
|
||||
plugins: [tailwindEsbuildPlugin],
|
||||
metafile: true,
|
||||
});
|
||||
const artifacts = Object.entries(build.metafile.outputs)
|
||||
.filter(([, meta]) => meta.entryPoint)
|
||||
.map(([outputPath, meta]) => {
|
||||
const cssPath = meta.cssBundle || undefined;
|
||||
return {
|
||||
path: outputPath,
|
||||
hash: path.basename(outputPath, path.extname(outputPath)),
|
||||
type: outputPath.endsWith(".css")
|
||||
? "text/css"
|
||||
: "text/javascript",
|
||||
css_path: cssPath,
|
||||
};
|
||||
});
|
||||
return artifacts?.[0];
|
||||
}
|
||||
catch (error) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
// import * as esbuild from "esbuild";
|
||||
// import path from "path";
|
||||
// import tailwindEsbuildPlugin from "./tailwind-esbuild-plugin";
|
||||
// const hmrExternalsPlugin: esbuild.Plugin = {
|
||||
// name: "hmr-globals",
|
||||
// setup(build) {
|
||||
// const mapping: Record<string, string> = {
|
||||
// react: "__REACT__",
|
||||
// "react-dom": "__REACT_DOM__",
|
||||
// "react-dom/client": "__REACT_DOM_CLIENT__",
|
||||
// "react/jsx-runtime": "__JSX_RUNTIME__",
|
||||
// };
|
||||
// const filter = new RegExp(
|
||||
// `^(${Object.keys(mapping)
|
||||
// .map((k) => k.replace("/", "\\/"))
|
||||
// .join("|")})$`,
|
||||
// );
|
||||
// build.onResolve({ filter }, (args) => {
|
||||
// return { path: args.path, namespace: "hmr-global" };
|
||||
// });
|
||||
// build.onLoad({ filter: /.*/, namespace: "hmr-global" }, (args) => {
|
||||
// const globalName = mapping[args.path];
|
||||
// return {
|
||||
// contents: `module.exports = window.${globalName};`,
|
||||
// loader: "js",
|
||||
// };
|
||||
// });
|
||||
// },
|
||||
// };
|
||||
// type Params = {
|
||||
// tsx: string;
|
||||
// file_path: string;
|
||||
// out_file: string;
|
||||
// };
|
||||
// export default async function writeHMRTsxModule({
|
||||
// tsx,
|
||||
// file_path,
|
||||
// out_file,
|
||||
// }: Params) {
|
||||
// try {
|
||||
// await esbuild.build({
|
||||
// stdin: {
|
||||
// contents: tsx,
|
||||
// resolveDir: path.dirname(file_path),
|
||||
// loader: "tsx",
|
||||
// },
|
||||
// bundle: true,
|
||||
// format: "esm",
|
||||
// target: "es2020",
|
||||
// platform: "browser",
|
||||
// minify: true,
|
||||
// jsx: "automatic",
|
||||
// outfile: out_file,
|
||||
// plugins: [hmrExternalsPlugin, tailwindEsbuildPlugin],
|
||||
// });
|
||||
// return true;
|
||||
// } catch (error) {
|
||||
// return false;
|
||||
// }
|
||||
// }
|
||||
1
dist/index.js
vendored
1
dist/index.js
vendored
@ -11,6 +11,7 @@ global.ORA_SPINNER.clear();
|
||||
global.HMR_CONTROLLERS = [];
|
||||
global.IS_FIRST_BUNDLE_READY = false;
|
||||
global.BUNDLER_REBUILDS = 0;
|
||||
global.PAGE_FILES = [];
|
||||
await init();
|
||||
const { PAGES_DIR } = grabDirNames();
|
||||
const router = new Bun.FileSystemRouter({
|
||||
|
||||
12
dist/utils/log.js
vendored
12
dist/utils/log.js
vendored
@ -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`),
|
||||
};
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -1,37 +1,16 @@
|
||||
import { existsSync, writeFileSync } from "fs";
|
||||
import path from "path";
|
||||
import { writeFileSync } from "fs";
|
||||
import * as esbuild from "esbuild";
|
||||
import postcss from "postcss";
|
||||
import tailwindcss from "@tailwindcss/postcss";
|
||||
import { readFile } from "fs/promises";
|
||||
import grabAllPages from "../../utils/grab-all-pages";
|
||||
import grabDirNames from "../../utils/grab-dir-names";
|
||||
import AppNames from "../../utils/grab-app-names";
|
||||
import isDevelopment from "../../utils/is-development";
|
||||
import type { BundlerCTXMap } from "../../types";
|
||||
import { execSync } from "child_process";
|
||||
import grabConstants from "../../utils/grab-constants";
|
||||
import { log } from "../../utils/log";
|
||||
import tailwindEsbuildPlugin from "../server/web-pages/tailwind-esbuild-plugin";
|
||||
import grabClientHydrationScript from "./grab-client-hydration-script";
|
||||
import grabArtifactsFromBundledResults from "./grab-artifacts-from-bundled-result";
|
||||
|
||||
const { HYDRATION_DST_DIR, PAGES_DIR, HYDRATION_DST_DIR_MAP_JSON_FILE } =
|
||||
grabDirNames();
|
||||
|
||||
const tailwindPlugin: esbuild.Plugin = {
|
||||
name: "tailwindcss",
|
||||
setup(build) {
|
||||
build.onLoad({ filter: /\.css$/ }, async (args) => {
|
||||
const source = await readFile(args.path, "utf-8");
|
||||
const result = await postcss([tailwindcss()]).process(source, {
|
||||
from: args.path,
|
||||
});
|
||||
|
||||
return {
|
||||
contents: result.css,
|
||||
loader: "css",
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
const { HYDRATION_DST_DIR, HYDRATION_DST_DIR_MAP_JSON_FILE } = grabDirNames();
|
||||
|
||||
type Params = {
|
||||
watch?: boolean;
|
||||
@ -41,37 +20,16 @@ type Params = {
|
||||
|
||||
export default async function allPagesBundler(params?: Params) {
|
||||
const pages = grabAllPages({ exclude_api: true });
|
||||
const { ClientRootElementIDName, ClientRootComponentWindowName } =
|
||||
grabConstants();
|
||||
|
||||
const virtualEntries: Record<string, string> = {};
|
||||
const dev = isDevelopment();
|
||||
|
||||
const root_component_path = path.join(
|
||||
PAGES_DIR,
|
||||
`${AppNames["RootPagesComponentName"]}.tsx`,
|
||||
);
|
||||
|
||||
const does_root_exist = existsSync(root_component_path);
|
||||
|
||||
for (const page of pages) {
|
||||
const key = page.local_path;
|
||||
|
||||
let txt = ``;
|
||||
txt += `import { hydrateRoot } from "react-dom/client";\n`;
|
||||
if (does_root_exist) {
|
||||
txt += `import Root from "${root_component_path}";\n`;
|
||||
}
|
||||
txt += `import Page from "${page.local_path}";\n\n`;
|
||||
txt += `const pageProps = window.__PAGE_PROPS__ || {};\n`;
|
||||
|
||||
if (does_root_exist) {
|
||||
txt += `const component = <Root {...pageProps}><Page {...pageProps} /></Root>\n`;
|
||||
} else {
|
||||
txt += `const component = <Page {...pageProps} />\n`;
|
||||
}
|
||||
txt += `const root = hydrateRoot(document.getElementById("${ClientRootElementIDName}"), component);\n\n`;
|
||||
txt += `window.${ClientRootComponentWindowName} = root;\n`;
|
||||
const txt = grabClientHydrationScript({
|
||||
page_local_path: page.local_path,
|
||||
});
|
||||
|
||||
virtualEntries[key] = txt;
|
||||
}
|
||||
@ -104,48 +62,15 @@ export default async function allPagesBundler(params?: Params) {
|
||||
build.onEnd((result) => {
|
||||
if (result.errors.length > 0) return;
|
||||
|
||||
const artifacts: (BundlerCTXMap | undefined)[] = Object.entries(
|
||||
result.metafile!.outputs,
|
||||
)
|
||||
.filter(([, meta]) => meta.entryPoint)
|
||||
.map(([outputPath, meta]) => {
|
||||
const target_page = pages.find((p) => {
|
||||
return (
|
||||
meta.entryPoint === `virtual:${p.local_path}`
|
||||
);
|
||||
});
|
||||
const artifacts = grabArtifactsFromBundledResults({
|
||||
pages,
|
||||
result,
|
||||
});
|
||||
|
||||
if (!target_page || !meta.entryPoint) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { file_name, local_path, url_path } = target_page;
|
||||
|
||||
const cssPath = meta.cssBundle || undefined;
|
||||
|
||||
return {
|
||||
path: outputPath,
|
||||
hash: path.basename(
|
||||
outputPath,
|
||||
path.extname(outputPath),
|
||||
),
|
||||
type: outputPath.endsWith(".css")
|
||||
? "text/css"
|
||||
: "text/javascript",
|
||||
entrypoint: meta.entryPoint,
|
||||
css_path: cssPath,
|
||||
file_name,
|
||||
local_path,
|
||||
url_path,
|
||||
};
|
||||
});
|
||||
|
||||
if (artifacts.length > 0) {
|
||||
const final_artifacts = artifacts.filter((a) =>
|
||||
Boolean(a?.entrypoint),
|
||||
) as BundlerCTXMap[];
|
||||
global.BUNDLER_CTX_MAP = final_artifacts;
|
||||
params?.post_build_fn?.({ artifacts: final_artifacts });
|
||||
if (artifacts?.[0] && artifacts.length > 0) {
|
||||
global.BUNDLER_CTX_MAP = artifacts;
|
||||
global.PAGE_FILES = pages;
|
||||
params?.post_build_fn?.({ artifacts });
|
||||
|
||||
writeFileSync(
|
||||
HYDRATION_DST_DIR_MAP_JSON_FILE,
|
||||
@ -154,7 +79,7 @@ export default async function allPagesBundler(params?: Params) {
|
||||
}
|
||||
|
||||
const elapsed = (performance.now() - buildStart).toFixed(0);
|
||||
log.success(`Built in ${elapsed}ms`);
|
||||
log.success(`[Built] in ${elapsed}ms`);
|
||||
|
||||
if (params?.exit_after_first_build) {
|
||||
process.exit();
|
||||
@ -180,9 +105,10 @@ export default async function allPagesBundler(params?: Params) {
|
||||
},
|
||||
entryNames: "[dir]/[name]/[hash]",
|
||||
metafile: true,
|
||||
plugins: [tailwindPlugin, virtualPlugin, artifactTracker],
|
||||
plugins: [tailwindEsbuildPlugin, virtualPlugin, artifactTracker],
|
||||
jsx: "automatic",
|
||||
splitting: true,
|
||||
logLevel: "silent",
|
||||
});
|
||||
|
||||
await ctx.rebuild();
|
||||
|
||||
56
src/functions/bundler/grab-artifacts-from-bundled-result.ts
Normal file
56
src/functions/bundler/grab-artifacts-from-bundled-result.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import path from "path";
|
||||
import * as esbuild from "esbuild";
|
||||
import type { BundlerCTXMap, PageFiles } from "../../types";
|
||||
|
||||
type Params = {
|
||||
result: esbuild.BuildResult<esbuild.BuildOptions>;
|
||||
pages: PageFiles[];
|
||||
};
|
||||
|
||||
export default function grabArtifactsFromBundledResults({
|
||||
result,
|
||||
pages,
|
||||
}: Params) {
|
||||
if (result.errors.length > 0) return;
|
||||
|
||||
const artifacts: (BundlerCTXMap | undefined)[] = Object.entries(
|
||||
result.metafile!.outputs,
|
||||
)
|
||||
.filter(([, meta]) => meta.entryPoint)
|
||||
.map(([outputPath, meta]) => {
|
||||
const target_page = pages.find((p) => {
|
||||
return meta.entryPoint === `virtual:${p.local_path}`;
|
||||
});
|
||||
|
||||
if (!target_page || !meta.entryPoint) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { file_name, local_path, url_path } = target_page;
|
||||
|
||||
const cssPath = meta.cssBundle || undefined;
|
||||
|
||||
return {
|
||||
path: outputPath,
|
||||
hash: path.basename(outputPath, path.extname(outputPath)),
|
||||
type: outputPath.endsWith(".css")
|
||||
? "text/css"
|
||||
: "text/javascript",
|
||||
entrypoint: meta.entryPoint,
|
||||
css_path: cssPath,
|
||||
file_name,
|
||||
local_path,
|
||||
url_path,
|
||||
};
|
||||
});
|
||||
|
||||
if (artifacts.length > 0) {
|
||||
const final_artifacts = artifacts.filter((a) =>
|
||||
Boolean(a?.entrypoint),
|
||||
) as BundlerCTXMap[];
|
||||
|
||||
return final_artifacts;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
84
src/functions/bundler/grab-client-hydration-script.ts
Normal file
84
src/functions/bundler/grab-client-hydration-script.ts
Normal file
@ -0,0 +1,84 @@
|
||||
import { existsSync } from "fs";
|
||||
import path from "path";
|
||||
import grabDirNames from "../../utils/grab-dir-names";
|
||||
import AppNames from "../../utils/grab-app-names";
|
||||
import grabConstants from "../../utils/grab-constants";
|
||||
|
||||
const { PAGES_DIR } = grabDirNames();
|
||||
|
||||
type Params = {
|
||||
page_local_path: string;
|
||||
};
|
||||
|
||||
export default function grabClientHydrationScript({ page_local_path }: Params) {
|
||||
const { ClientRootElementIDName, ClientRootComponentWindowName } =
|
||||
grabConstants();
|
||||
|
||||
const root_component_path = path.join(
|
||||
PAGES_DIR,
|
||||
`${AppNames["RootPagesComponentName"]}.tsx`,
|
||||
);
|
||||
|
||||
const does_root_exist = existsSync(root_component_path);
|
||||
|
||||
// let txt = ``;
|
||||
// txt += `import { hydrateRoot } from "react-dom/client";\n`;
|
||||
// if (does_root_exist) {
|
||||
// txt += `import Root from "${root_component_path}";\n`;
|
||||
// }
|
||||
// txt += `import Page from "${page.local_path}";\n\n`;
|
||||
// txt += `const pageProps = window.__PAGE_PROPS__ || {};\n`;
|
||||
|
||||
// if (does_root_exist) {
|
||||
// txt += `const component = <Root {...pageProps}><Page {...pageProps} /></Root>\n`;
|
||||
// } else {
|
||||
// txt += `const component = <Page {...pageProps} />\n`;
|
||||
// }
|
||||
// txt += `const root = hydrateRoot(document.getElementById("${ClientRootElementIDName}"), component);\n\n`;
|
||||
// txt += `window.${ClientRootComponentWindowName} = root;\n`;
|
||||
|
||||
let txt = ``;
|
||||
// txt += `import * as React from "react";\n`;
|
||||
// txt += `import * as ReactDOM from "react-dom";\n`;
|
||||
// txt += `import * as ReactDOMClient from "react-dom/client";\n`;
|
||||
// txt += `import * as JSXRuntime from "react/jsx-runtime";\n`;
|
||||
txt += `import { hydrateRoot, createElement } from "react-dom/client";\n`;
|
||||
if (does_root_exist) {
|
||||
txt += `import Root from "${root_component_path}";\n`;
|
||||
}
|
||||
txt += `import Page from "${page_local_path}";\n\n`;
|
||||
// txt += `window.__REACT__ = React;\n`;
|
||||
// txt += `window.__REACT_DOM__ = ReactDOM;\n`;
|
||||
// txt += `window.__REACT_DOM_CLIENT__ = ReactDOMClient;\n`;
|
||||
// txt += `window.__JSX_RUNTIME__ = JSXRuntime;\n\n`;
|
||||
txt += `const pageProps = window.__PAGE_PROPS__ || {};\n`;
|
||||
|
||||
if (does_root_exist) {
|
||||
txt += `const component = <Root {...pageProps}><Page {...pageProps} /></Root>\n`;
|
||||
} else {
|
||||
txt += `const component = <Page {...pageProps} />\n`;
|
||||
}
|
||||
|
||||
txt += `if (window.${ClientRootComponentWindowName}?.render) {\n`;
|
||||
txt += ` window.${ClientRootComponentWindowName}.render(component);\n`;
|
||||
txt += `} else {\n`;
|
||||
txt += ` const root = hydrateRoot(document.getElementById("${ClientRootElementIDName}"), component);\n\n`;
|
||||
txt += ` window.${ClientRootComponentWindowName} = root;\n`;
|
||||
|
||||
txt += ` window.__BUNEXT_RERENDER__ = (NewPage) => {\n`;
|
||||
txt += ` const props = window.__PAGE_PROPS__ || {};\n`;
|
||||
txt += ` root.render(<NewPage {...props} />);\n`;
|
||||
txt += ` };\n`;
|
||||
txt += `}\n`;
|
||||
|
||||
// // HMR re-render helper
|
||||
// if (does_root_exist) {
|
||||
// txt += `window.__BUNEXT_RERENDER__ = (NewPage) => {\n`;
|
||||
// txt += ` const props = window.__PAGE_PROPS__ || {};\n`;
|
||||
// txt += ` root.render(<Root {...props}><NewPage {...props} /></Root>);\n`;
|
||||
// txt += `};\n`;
|
||||
// } else {
|
||||
// }
|
||||
|
||||
return txt;
|
||||
}
|
||||
@ -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
|
||||
>)[];
|
||||
|
||||
33
src/functions/server/handle-files.ts
Normal file
33
src/functions/server/handle-files.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import type { Server } from "bun";
|
||||
import grabDirNames from "../../utils/grab-dir-names";
|
||||
import path from "path";
|
||||
import isDevelopment from "../../utils/is-development";
|
||||
import { existsSync } from "fs";
|
||||
|
||||
const { PUBLIC_DIR } = grabDirNames();
|
||||
|
||||
type Params = {
|
||||
req: Request;
|
||||
server: Server;
|
||||
};
|
||||
|
||||
export default async function ({ req, server }: Params): Promise<Response> {
|
||||
try {
|
||||
const is_dev = isDevelopment();
|
||||
const url = new URL(req.url);
|
||||
const file_path = path.join(PUBLIC_DIR, url.pathname);
|
||||
|
||||
if (!existsSync(file_path)) {
|
||||
return new Response(`File Doesn't Exist`, {
|
||||
status: 404,
|
||||
});
|
||||
}
|
||||
|
||||
const file = Bun.file(file_path);
|
||||
return new Response(file);
|
||||
} catch (error) {
|
||||
return new Response(`File Not Found`, {
|
||||
status: 404,
|
||||
});
|
||||
}
|
||||
}
|
||||
84
src/functions/server/handle-hmr-update.ts
Normal file
84
src/functions/server/handle-hmr-update.ts
Normal file
@ -0,0 +1,84 @@
|
||||
import type { Server } from "bun";
|
||||
import grabDirNames from "../../utils/grab-dir-names";
|
||||
import { AppData } from "../../data/app-data";
|
||||
import path from "path";
|
||||
import grabRootFile from "./web-pages/grab-root-file";
|
||||
import grabPageBundledReactComponent from "./web-pages/grab-page-bundled-react-component";
|
||||
import writeHMRTsxModule from "./web-pages/write-hmr-tsx-module";
|
||||
|
||||
const { PUBLIC_DIR, BUNX_HYDRATION_SRC_DIR } = grabDirNames();
|
||||
|
||||
type Params = {
|
||||
req: Request;
|
||||
server: Server;
|
||||
};
|
||||
|
||||
export default async function ({ req, server }: Params): Promise<Response> {
|
||||
try {
|
||||
const url = new URL(req.url);
|
||||
|
||||
const target_href = url.searchParams.get("href");
|
||||
|
||||
if (!target_href) {
|
||||
return new Response(
|
||||
`No HREF passed to /${AppData["ClientHMRPath"]}`,
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
|
||||
const target_href_url = new URL(target_href);
|
||||
|
||||
const match = global.ROUTER.match(target_href_url.pathname);
|
||||
|
||||
if (!match?.filePath) {
|
||||
return new Response(`No pages file matched for this path`, {
|
||||
status: 404,
|
||||
});
|
||||
}
|
||||
|
||||
const out_file = path.join(
|
||||
BUNX_HYDRATION_SRC_DIR,
|
||||
target_href_url.pathname,
|
||||
"index.js",
|
||||
);
|
||||
|
||||
const { root_file } = grabRootFile();
|
||||
|
||||
const { tsx } =
|
||||
(await grabPageBundledReactComponent({
|
||||
file_path: match.filePath,
|
||||
root_file,
|
||||
})) || {};
|
||||
|
||||
if (!tsx) {
|
||||
throw new Error(`Couldn't grab txt string`);
|
||||
}
|
||||
|
||||
const artifact = await writeHMRTsxModule({
|
||||
tsx,
|
||||
out_file,
|
||||
});
|
||||
|
||||
const file = Bun.file(out_file);
|
||||
|
||||
if (await file.exists()) {
|
||||
return new Response(file, {
|
||||
headers: {
|
||||
"Content-Type": "text/javascript",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return new Response("Not found", {
|
||||
status: 404,
|
||||
});
|
||||
} catch (error: any) {
|
||||
const error_msg = error.message;
|
||||
|
||||
console.error(error_msg);
|
||||
|
||||
return new Response(error_msg || "HMR Error", {
|
||||
status: 404,
|
||||
});
|
||||
}
|
||||
}
|
||||
50
src/functions/server/handle-hmr.ts
Normal file
50
src/functions/server/handle-hmr.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import type { Server } from "bun";
|
||||
import type { BunextServerRouteConfig, BunxRouteParams } from "../../types";
|
||||
import grabRouteParams from "../../utils/grab-route-params";
|
||||
import grabConstants from "../../utils/grab-constants";
|
||||
import grabRouter from "../../utils/grab-router";
|
||||
|
||||
type Params = {
|
||||
req: Request;
|
||||
server: Server;
|
||||
};
|
||||
|
||||
export default async function ({ req, server }: Params): Promise<Response> {
|
||||
const referer_url = new URL(req.headers.get("referer") || "");
|
||||
const match = global.ROUTER.match(referer_url.pathname);
|
||||
|
||||
const target_map = match?.filePath
|
||||
? global.BUNDLER_CTX_MAP?.find((m) => m.local_path == match.filePath)
|
||||
: undefined;
|
||||
|
||||
let controller: ReadableStreamDefaultController<string>;
|
||||
const stream = new ReadableStream<string>({
|
||||
start(c) {
|
||||
controller = c;
|
||||
global.HMR_CONTROLLERS.push({
|
||||
controller: c,
|
||||
page_url: referer_url.href,
|
||||
target_map,
|
||||
});
|
||||
},
|
||||
cancel() {
|
||||
const targetControllerIndex = global.HMR_CONTROLLERS.findIndex(
|
||||
(c) => c.controller == controller,
|
||||
);
|
||||
|
||||
if (
|
||||
typeof targetControllerIndex == "number" &&
|
||||
targetControllerIndex >= 0
|
||||
) {
|
||||
global.HMR_CONTROLLERS.splice(targetControllerIndex, 1);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
"Content-Type": "text/event-stream",
|
||||
Connection: "keep-alive",
|
||||
},
|
||||
});
|
||||
}
|
||||
40
src/functions/server/handle-public.ts
Normal file
40
src/functions/server/handle-public.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import type { Server } from "bun";
|
||||
import grabDirNames from "../../utils/grab-dir-names";
|
||||
import path from "path";
|
||||
import isDevelopment from "../../utils/is-development";
|
||||
import { existsSync } from "fs";
|
||||
|
||||
const { PUBLIC_DIR, BUNX_HYDRATION_SRC_DIR } = grabDirNames();
|
||||
|
||||
type Params = {
|
||||
req: Request;
|
||||
server: Server;
|
||||
};
|
||||
|
||||
export default async function ({ req, server }: Params): Promise<Response> {
|
||||
try {
|
||||
const is_dev = isDevelopment();
|
||||
const url = new URL(req.url);
|
||||
|
||||
const file_path = path.join(
|
||||
PUBLIC_DIR,
|
||||
url.pathname.replace(/^\/public/, ""),
|
||||
);
|
||||
|
||||
if (!existsSync(file_path)) {
|
||||
return new Response(`Public File Doesn't Exist`, {
|
||||
status: 404,
|
||||
});
|
||||
}
|
||||
|
||||
const file = Bun.file(file_path);
|
||||
|
||||
let res_opts: ResponseInit = {};
|
||||
|
||||
return new Response(file, res_opts);
|
||||
} catch (error) {
|
||||
return new Response(`Public File Not Found`, {
|
||||
status: 404,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,13 +1,14 @@
|
||||
import path from "path";
|
||||
import type { ServeOptions } from "bun";
|
||||
import grabAppPort from "../../utils/grab-app-port";
|
||||
import grabDirNames from "../../utils/grab-dir-names";
|
||||
import handleWebPages from "./web-pages/handle-web-pages";
|
||||
import handleRoutes from "./handle-routes";
|
||||
import isDevelopment from "../../utils/is-development";
|
||||
import grabConstants from "../../utils/grab-constants";
|
||||
import { AppData } from "../../data/app-data";
|
||||
import { existsSync } from "fs";
|
||||
import handleHmr from "./handle-hmr";
|
||||
import handleHmrUpdate from "./handle-hmr-update";
|
||||
import handlePublic from "./handle-public";
|
||||
import handleFiles from "./handle-files";
|
||||
|
||||
type Params = {
|
||||
dev?: boolean;
|
||||
@ -15,7 +16,6 @@ type Params = {
|
||||
|
||||
export default async function (params?: Params): Promise<ServeOptions> {
|
||||
const port = grabAppPort();
|
||||
const { PUBLIC_DIR } = grabDirNames();
|
||||
|
||||
const is_dev = isDevelopment();
|
||||
|
||||
@ -26,6 +26,8 @@ export default async function (params?: Params): Promise<ServeOptions> {
|
||||
|
||||
const { config } = grabConstants();
|
||||
|
||||
let response: Response | undefined = undefined;
|
||||
|
||||
if (config?.middleware) {
|
||||
const middleware_res = await config.middleware({
|
||||
req,
|
||||
@ -38,109 +40,32 @@ export default async function (params?: Params): Promise<ServeOptions> {
|
||||
}
|
||||
}
|
||||
|
||||
if (url.pathname === "/__hmr" && is_dev) {
|
||||
const referer_url = new URL(
|
||||
req.headers.get("referer") || "",
|
||||
if (url.pathname == `/${AppData["ClientHMRPath"]}`) {
|
||||
response = await handleHmrUpdate({ req, server });
|
||||
} else if (url.pathname === "/__hmr" && is_dev) {
|
||||
response = await handleHmr({ req, server });
|
||||
} else if (url.pathname.startsWith("/api/")) {
|
||||
response = await handleRoutes({ req, server });
|
||||
} else if (url.pathname.startsWith("/public/")) {
|
||||
response = await handlePublic({ req, server });
|
||||
} else if (url.pathname.match(/\..*$/)) {
|
||||
response = await handleFiles({ req, server });
|
||||
} else {
|
||||
response = await handleWebPages({ req });
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
throw new Error(`No Response generated`);
|
||||
}
|
||||
|
||||
if (is_dev) {
|
||||
response.headers.set(
|
||||
"Cache-Control",
|
||||
"no-cache, no-store, must-revalidate",
|
||||
);
|
||||
const match = global.ROUTER.match(referer_url.pathname);
|
||||
|
||||
const target_map = match?.filePath
|
||||
? global.BUNDLER_CTX_MAP?.find(
|
||||
(m) => m.local_path == match.filePath,
|
||||
)
|
||||
: undefined;
|
||||
|
||||
let controller: ReadableStreamDefaultController<string>;
|
||||
const stream = new ReadableStream<string>({
|
||||
start(c) {
|
||||
controller = c;
|
||||
global.HMR_CONTROLLERS.push({
|
||||
controller: c,
|
||||
page_url: referer_url.href,
|
||||
target_map,
|
||||
});
|
||||
},
|
||||
cancel() {
|
||||
const targetControllerIndex =
|
||||
global.HMR_CONTROLLERS.findIndex(
|
||||
(c) => c.controller == controller,
|
||||
);
|
||||
|
||||
if (
|
||||
typeof targetControllerIndex == "number" &&
|
||||
targetControllerIndex >= 0
|
||||
) {
|
||||
global.HMR_CONTROLLERS.splice(
|
||||
targetControllerIndex,
|
||||
1,
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache",
|
||||
Connection: "keep-alive",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (url.pathname.startsWith("/api/")) {
|
||||
return await handleRoutes({ req, server });
|
||||
}
|
||||
|
||||
if (url.pathname.startsWith("/public/")) {
|
||||
try {
|
||||
const file_path = path.join(
|
||||
PUBLIC_DIR,
|
||||
url.pathname.replace(/^\/public/, ""),
|
||||
);
|
||||
|
||||
if (!existsSync(file_path)) {
|
||||
return new Response(`Public File Doesn't Exist`, {
|
||||
status: 404,
|
||||
});
|
||||
}
|
||||
|
||||
const file = Bun.file(file_path);
|
||||
|
||||
let res_opts: ResponseInit = {};
|
||||
|
||||
if (!is_dev && url.pathname.match(/__bunext/)) {
|
||||
res_opts.headers = {
|
||||
"Cache-Control": `public, max-age=${AppData["BunextStaticFilesCacheExpiry"]}, must-revalidate`,
|
||||
};
|
||||
}
|
||||
|
||||
return new Response(file, res_opts);
|
||||
} catch (error) {
|
||||
return new Response(`Public File Not Found`, {
|
||||
status: 404,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// if (url.pathname.startsWith("/favicon.") ) {
|
||||
if (url.pathname.match(/\..*$/)) {
|
||||
try {
|
||||
const file_path = path.join(PUBLIC_DIR, url.pathname);
|
||||
|
||||
if (!existsSync(file_path)) {
|
||||
return new Response(`File Doesn't Exist`, {
|
||||
status: 404,
|
||||
});
|
||||
}
|
||||
|
||||
const file = Bun.file(file_path);
|
||||
return new Response(file);
|
||||
} catch (error) {
|
||||
return new Response(`File Not Found`, { status: 404 });
|
||||
}
|
||||
}
|
||||
|
||||
return await handleWebPages({ req });
|
||||
return response;
|
||||
} catch (error: any) {
|
||||
return new Response(`Server Error: ${error.message}`, {
|
||||
status: 500,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -5,6 +5,8 @@ import type { LivePageDistGenParams } from "../../../types";
|
||||
import isDevelopment from "../../../utils/is-development";
|
||||
import grabWebPageHydrationScript from "./grab-web-page-hydration-script";
|
||||
import grabWebMetaHTML from "./grab-web-meta-html";
|
||||
import { log } from "../../../utils/log";
|
||||
import { AppData } from "../../../data/app-data";
|
||||
|
||||
export default async function genWebHTML({
|
||||
component,
|
||||
@ -14,6 +16,7 @@ export default async function genWebHTML({
|
||||
module,
|
||||
meta,
|
||||
routeParams,
|
||||
debug,
|
||||
}: LivePageDistGenParams) {
|
||||
const { ClientRootElementIDName, ClientWindowPagePropsName } =
|
||||
grabContants();
|
||||
@ -22,7 +25,16 @@ export default async function genWebHTML({
|
||||
path.join(process.cwd(), "node_modules", "react-dom", "server")
|
||||
);
|
||||
|
||||
if (debug) {
|
||||
log.info("component", component);
|
||||
}
|
||||
|
||||
const componentHTML = renderToString(component);
|
||||
|
||||
if (debug) {
|
||||
log.info("componentHTML", componentHTML);
|
||||
}
|
||||
|
||||
const headHTML = Head
|
||||
? renderToString(<Head serverRes={pageProps} ctx={routeParams} />)
|
||||
: "";
|
||||
@ -46,7 +58,7 @@ export default async function genWebHTML({
|
||||
}</script>\n`;
|
||||
|
||||
if (bundledMap?.path) {
|
||||
html += ` <script src="/${bundledMap.path}" type="module" async></script>\n`;
|
||||
html += ` <script src="/${bundledMap.path}" type="module" id="${AppData["BunextClientHydrationScriptID"]}" async></script>\n`;
|
||||
}
|
||||
|
||||
if (isDevelopment()) {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -5,37 +5,22 @@ import tailwindcss from "@tailwindcss/postcss";
|
||||
import { readFile } from "fs/promises";
|
||||
import grabDirNames from "../../../utils/grab-dir-names";
|
||||
import path from "path";
|
||||
import tailwindEsbuildPlugin from "./tailwind-esbuild-plugin";
|
||||
|
||||
type Params = {
|
||||
file_path: string;
|
||||
};
|
||||
|
||||
const tailwindPlugin: esbuild.Plugin = {
|
||||
name: "tailwindcss",
|
||||
setup(build) {
|
||||
build.onLoad({ filter: /\.css$/ }, async (args) => {
|
||||
const source = await readFile(args.path, "utf-8");
|
||||
const result = await postcss([tailwindcss()]).process(source, {
|
||||
from: args.path,
|
||||
});
|
||||
|
||||
return {
|
||||
contents: result.css,
|
||||
loader: "css",
|
||||
};
|
||||
});
|
||||
},
|
||||
out_file?: string;
|
||||
};
|
||||
|
||||
export default async function grabFilePathModule<T extends any = any>({
|
||||
file_path,
|
||||
out_file,
|
||||
}: Params): Promise<T> {
|
||||
const dev = isDevelopment();
|
||||
const { BUNX_CWD_MODULE_CACHE_DIR } = grabDirNames();
|
||||
const target_cache_file_path = path.join(
|
||||
BUNX_CWD_MODULE_CACHE_DIR,
|
||||
`${path.basename(file_path)}.js`,
|
||||
);
|
||||
const target_cache_file_path =
|
||||
out_file ||
|
||||
path.join(BUNX_CWD_MODULE_CACHE_DIR, `${path.basename(file_path)}.js`);
|
||||
|
||||
await esbuild.build({
|
||||
entryPoints: [file_path],
|
||||
@ -51,7 +36,7 @@ export default async function grabFilePathModule<T extends any = any>({
|
||||
),
|
||||
},
|
||||
metafile: true,
|
||||
plugins: [tailwindPlugin],
|
||||
plugins: [tailwindEsbuildPlugin],
|
||||
jsx: "automatic",
|
||||
outfile: target_cache_file_path,
|
||||
});
|
||||
|
||||
@ -30,9 +30,9 @@ export default async function grabPageBundledReactComponent({
|
||||
tsx += `const props = JSON.parse("${server_res_json}")\n\n`;
|
||||
tsx += ` return (\n`;
|
||||
if (root_file) {
|
||||
tsx += ` <Root {...props}><Page {...props} /></Root>\n`;
|
||||
tsx += ` <Root suppressHydrationWarning={true} {...props}><Page {...props} /></Root>\n`;
|
||||
} else {
|
||||
tsx += ` <Page {...props} />\n`;
|
||||
tsx += ` <Page suppressHydrationWarning={true} {...props} />\n`;
|
||||
}
|
||||
tsx += ` )\n`;
|
||||
tsx += `}\n`;
|
||||
@ -44,6 +44,7 @@ export default async function grabPageBundledReactComponent({
|
||||
return {
|
||||
component,
|
||||
server_res,
|
||||
tsx,
|
||||
};
|
||||
} catch (error: any) {
|
||||
return undefined;
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import type { FC } from "react";
|
||||
import grabDirNames from "../../../utils/grab-dir-names";
|
||||
import grabRouteParams from "../../../utils/grab-route-params";
|
||||
import type {
|
||||
BunextPageModule,
|
||||
@ -7,29 +6,28 @@ import type {
|
||||
BunxRouteParams,
|
||||
GrabPageComponentRes,
|
||||
} from "../../../types";
|
||||
import path from "path";
|
||||
import AppNames from "../../../utils/grab-app-names";
|
||||
import { existsSync } from "fs";
|
||||
import grabPageErrorComponent from "./grab-page-error-component";
|
||||
import grabPageBundledReactComponent from "./grab-page-bundled-react-component";
|
||||
import _ from "lodash";
|
||||
import { log } from "../../../utils/log";
|
||||
import grabRootFile from "./grab-root-file";
|
||||
|
||||
class NotFoundError extends Error {}
|
||||
|
||||
type Params = {
|
||||
req?: Request;
|
||||
file_path?: string;
|
||||
debug?: boolean;
|
||||
};
|
||||
|
||||
export default async function grabPageComponent({
|
||||
req,
|
||||
file_path: passed_file_path,
|
||||
debug,
|
||||
}: Params): Promise<GrabPageComponentRes> {
|
||||
const url = req?.url ? new URL(req.url) : undefined;
|
||||
const router = global.ROUTER;
|
||||
|
||||
const { PAGES_DIR } = grabDirNames();
|
||||
|
||||
let routeParams: BunxRouteParams | undefined = undefined;
|
||||
|
||||
try {
|
||||
@ -41,6 +39,10 @@ export default async function grabPageComponent({
|
||||
url_path += url.search;
|
||||
}
|
||||
|
||||
if (debug) {
|
||||
log.info(`url_path:`, url_path);
|
||||
}
|
||||
|
||||
const match = url_path ? router.match(url_path) : undefined;
|
||||
|
||||
if (!match?.filePath && url?.pathname) {
|
||||
@ -49,6 +51,10 @@ export default async function grabPageComponent({
|
||||
|
||||
const file_path = match?.filePath || passed_file_path;
|
||||
|
||||
if (debug) {
|
||||
log.info(`file_path:`, file_path);
|
||||
}
|
||||
|
||||
if (!file_path) {
|
||||
const errMsg = `No File Path (\`file_path\`) or Request Object (\`req\`) provided not found`;
|
||||
// console.error(errMsg);
|
||||
@ -65,25 +71,18 @@ export default async function grabPageComponent({
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
|
||||
const root_pages_component_ts_file = `${path.join(PAGES_DIR, AppNames["RootPagesComponentName"])}.ts`;
|
||||
const root_pages_component_tsx_file = `${path.join(PAGES_DIR, AppNames["RootPagesComponentName"])}.tsx`;
|
||||
const root_pages_component_js_file = `${path.join(PAGES_DIR, AppNames["RootPagesComponentName"])}.js`;
|
||||
const root_pages_component_jsx_file = `${path.join(PAGES_DIR, AppNames["RootPagesComponentName"])}.jsx`;
|
||||
if (debug) {
|
||||
log.info(`bundledMap:`, bundledMap);
|
||||
}
|
||||
|
||||
const root_file = existsSync(root_pages_component_tsx_file)
|
||||
? root_pages_component_tsx_file
|
||||
: existsSync(root_pages_component_ts_file)
|
||||
? root_pages_component_ts_file
|
||||
: existsSync(root_pages_component_jsx_file)
|
||||
? root_pages_component_jsx_file
|
||||
: existsSync(root_pages_component_js_file)
|
||||
? root_pages_component_js_file
|
||||
: undefined;
|
||||
|
||||
const now = Date.now();
|
||||
const { root_file } = grabRootFile();
|
||||
|
||||
const module: BunextPageModule = await import(file_path);
|
||||
|
||||
if (debug) {
|
||||
log.info(`module:`, module);
|
||||
}
|
||||
|
||||
const serverRes: BunextPageModuleServerReturn = await (async () => {
|
||||
const default_props: BunextPageModuleServerReturn = {
|
||||
url: {
|
||||
@ -123,6 +122,10 @@ export default async function grabPageComponent({
|
||||
}
|
||||
})();
|
||||
|
||||
if (debug) {
|
||||
log.info(`serverRes:`, serverRes);
|
||||
}
|
||||
|
||||
const meta = module.meta
|
||||
? typeof module.meta == "function" && routeParams
|
||||
? await module.meta({
|
||||
@ -134,6 +137,10 @@ export default async function grabPageComponent({
|
||||
: undefined
|
||||
: undefined;
|
||||
|
||||
if (debug) {
|
||||
log.info(`meta:`, meta);
|
||||
}
|
||||
|
||||
const Head = module.Head as FC<any>;
|
||||
|
||||
const { component } =
|
||||
@ -147,6 +154,10 @@ export default async function grabPageComponent({
|
||||
throw new Error(`Couldn't grab page component`);
|
||||
}
|
||||
|
||||
if (debug) {
|
||||
log.info(`component:`, component);
|
||||
}
|
||||
|
||||
return {
|
||||
component,
|
||||
serverRes,
|
||||
@ -157,6 +168,8 @@ export default async function grabPageComponent({
|
||||
head: Head,
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.error(`Error Grabbing Page Component: ${error.message}`);
|
||||
|
||||
return await grabPageErrorComponent({
|
||||
error,
|
||||
routeParams,
|
||||
|
||||
25
src/functions/server/web-pages/grab-root-file.tsx
Normal file
25
src/functions/server/web-pages/grab-root-file.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import grabDirNames from "../../../utils/grab-dir-names";
|
||||
import path from "path";
|
||||
import AppNames from "../../../utils/grab-app-names";
|
||||
import { existsSync } from "fs";
|
||||
|
||||
export default function grabRootFile() {
|
||||
const { PAGES_DIR } = grabDirNames();
|
||||
|
||||
const root_pages_component_ts_file = `${path.join(PAGES_DIR, AppNames["RootPagesComponentName"])}.ts`;
|
||||
const root_pages_component_tsx_file = `${path.join(PAGES_DIR, AppNames["RootPagesComponentName"])}.tsx`;
|
||||
const root_pages_component_js_file = `${path.join(PAGES_DIR, AppNames["RootPagesComponentName"])}.js`;
|
||||
const root_pages_component_jsx_file = `${path.join(PAGES_DIR, AppNames["RootPagesComponentName"])}.jsx`;
|
||||
|
||||
const root_file = existsSync(root_pages_component_tsx_file)
|
||||
? root_pages_component_tsx_file
|
||||
: existsSync(root_pages_component_ts_file)
|
||||
? root_pages_component_ts_file
|
||||
: existsSync(root_pages_component_jsx_file)
|
||||
? root_pages_component_jsx_file
|
||||
: existsSync(root_pages_component_js_file)
|
||||
? root_pages_component_js_file
|
||||
: undefined;
|
||||
|
||||
return { root_file };
|
||||
}
|
||||
@ -6,29 +6,13 @@ import { readFile } from "fs/promises";
|
||||
import grabDirNames from "../../../utils/grab-dir-names";
|
||||
import path from "path";
|
||||
import { execSync } from "child_process";
|
||||
import tailwindEsbuildPlugin from "./tailwind-esbuild-plugin";
|
||||
|
||||
type Params = {
|
||||
tsx: string;
|
||||
file_path: string;
|
||||
};
|
||||
|
||||
const tailwindPlugin: esbuild.Plugin = {
|
||||
name: "tailwindcss",
|
||||
setup(build) {
|
||||
build.onLoad({ filter: /\.css$/ }, async (args) => {
|
||||
const source = await readFile(args.path, "utf-8");
|
||||
const result = await postcss([tailwindcss()]).process(source, {
|
||||
from: args.path,
|
||||
});
|
||||
|
||||
return {
|
||||
contents: result.css,
|
||||
loader: "css",
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default async function grabTsxStringModule<T extends any = any>({
|
||||
tsx,
|
||||
file_path,
|
||||
@ -63,7 +47,7 @@ export default async function grabTsxStringModule<T extends any = any>({
|
||||
),
|
||||
},
|
||||
metafile: true,
|
||||
plugins: [tailwindPlugin],
|
||||
plugins: [tailwindEsbuildPlugin],
|
||||
jsx: "automatic",
|
||||
write: true,
|
||||
outfile: out_file_path,
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
import grabDirNames from "../../../utils/grab-dir-names";
|
||||
import type { BundlerCTXMap, PageDistGenParams } from "../../../types";
|
||||
|
||||
const { BUNX_HYDRATION_SRC_DIR } = grabDirNames();
|
||||
import type { BundlerCTXMap } from "../../../types";
|
||||
import { AppData } from "../../../data/app-data";
|
||||
|
||||
type Params = {
|
||||
bundledMap?: BundlerCTXMap;
|
||||
@ -10,62 +8,127 @@ type Params = {
|
||||
export default async function ({ bundledMap }: Params) {
|
||||
let script = "";
|
||||
|
||||
// script += `import React from "react";\n`;
|
||||
// script += `import { hydrateRoot } from "react-dom/client";\n`;
|
||||
// script += `import App from "${page_file}";\n`;
|
||||
|
||||
// script += `declare global {\n`;
|
||||
// script += ` interface Window {\n`;
|
||||
// script += ` ${ClientWindowPagePropsName}: any;\n`;
|
||||
// script += ` }\n`;
|
||||
// script += `}\n`;
|
||||
|
||||
// script += `let root: any = null;\n\n`;
|
||||
// script += `const component = <App {...window.${ClientWindowPagePropsName}} />;\n\n`;
|
||||
// script += `const container = document.getElementById("${ClientRootElementIDName}");\n\n`;
|
||||
// script += `if (container) {\n`;
|
||||
// script += ` root = hydrateRoot(container, component);\n`;
|
||||
// script += `}\n\n`;
|
||||
script += `console.log(\`Development Environment\`);\n`;
|
||||
// script += `console.log(import.meta);\n`;
|
||||
|
||||
// script += `if (import.meta.hot) {\n`;
|
||||
// script += ` console.log(\`HMR active\`);\n`;
|
||||
// script += ` import.meta.hot.dispose(() => {\n`;
|
||||
// script += ` console.log("dispose");\n`;
|
||||
// script += ` });\n`;
|
||||
// script += `}\n`;
|
||||
script += `console.log(\`Development Environment\`);\n\n`;
|
||||
|
||||
script += `const hmr = new EventSource("/__hmr");\n`;
|
||||
script += `hmr.addEventListener("update", async (event) => {\n`;
|
||||
// script += ` console.log(\`HMR even received:\`, event);\n`;
|
||||
script += ` if (event.data) {\n`;
|
||||
script += ` console.log(\`HMR Changes Detected. Reloading ...\`);\n`;
|
||||
// script += ` console.log("event", event);\n`;
|
||||
// script += ` console.log("window.${ClientRootComponentWindowName}", window.${ClientRootComponentWindowName});\n\n`;
|
||||
// script += ` const event_data = JSON.parse(event.data);\n\n`;
|
||||
// script += ` const new_js_path = \`/\${event_data.target_map.path}\`;\n\n`;
|
||||
script += ` console.log(\`HMR Changes Detected. Updating ...\`);\n`;
|
||||
script += ` try {\n`;
|
||||
script += ` const data = JSON.parse(event.data);\n`;
|
||||
// script += ` console.log("data", data);\n`;
|
||||
// script += ` const modulePath = \`/\${data.target_map.path}\`;\n\n`;
|
||||
|
||||
// script += ` console.log("event_data", event_data);\n\n`;
|
||||
// script += ` console.log("new_js_path", new_js_path);\n\n`;
|
||||
// script += ` const modulePath = \`/${AppData["ClientHMRPath"]}?href=\${window.location.href}&t=\${Date.now()}\`;\n\n`;
|
||||
// script += ` console.log("Fetching updated module ...", modulePath);\n\n`;
|
||||
// script += ` const newModule = await import(modulePath);\n\n`;
|
||||
// script += ` console.log("newModule", newModule);\n\n`;
|
||||
// script += ` if (window.__BUNEXT_RERENDER__ && newModule.default) {\n`;
|
||||
// script += ` window.__BUNEXT_RERENDER__(newModule.default);\n`;
|
||||
// script += ` console.log(\`HMR: Component updated in-place\`);\n`;
|
||||
// script += ` } else {\n`;
|
||||
// script += ` console.warn(\`HMR: No re-render helper found, falling back to reload\`);\n`;
|
||||
// // script += ` window.location.reload();\n`;
|
||||
// script += ` }\n\n`;
|
||||
|
||||
// script += ` if (window.${ClientRootComponentWindowName}) {\n`;
|
||||
// script += ` const new_component = await import(new_js_path);\n`;
|
||||
// script += ` window.${ClientRootComponentWindowName}.render(new_component);\n`;
|
||||
// script += ` }\n`;
|
||||
script += ` if (data.target_map.css_path) {\n`;
|
||||
script += ` const oldLink = document.querySelector('link[rel="stylesheet"]');\n`;
|
||||
script += ` const newLink = document.createElement("link");\n`;
|
||||
script += ` newLink.rel = "stylesheet";\n`;
|
||||
script += ` newLink.href = \`/\${data.target_map.css_path}?t=\${Date.now()}\`;\n`;
|
||||
script += ` newLink.onload = () => oldLink?.remove();\n`;
|
||||
script += ` document.head.appendChild(newLink);\n`;
|
||||
script += ` }\n`;
|
||||
|
||||
// script += ` import("${page_file}?t=" + event.data.update).then((module) => {\n`;
|
||||
// script += ` root.render(module.default);\n`;
|
||||
// script += ` })\n`;
|
||||
// script += ` console.log("root", root);\n`;
|
||||
// script += ` root.unmount();\n`;
|
||||
// script += ` const container = document.getElementById("${ClientRootElementIDName}");\n\n`;
|
||||
// script += ` root = hydrateRoot(container!, component);\n`;
|
||||
// script += ` window.history.pushState({ page: 1 }, "New Page Title", \`\${window.location.pathname}?v=\${Date.now()}\`);\n`;
|
||||
// script += ` root.render(component);\n`;
|
||||
script += ` window.location.reload();\n`;
|
||||
script += ` const newScriptPath = \`/\${data.target_map.path}?t=\${Date.now()}\`;\n\n`;
|
||||
script += ` const oldScript = document.getElementById("${AppData["BunextClientHydrationScriptID"]}");\n`;
|
||||
script += ` if (oldScript) {\n`;
|
||||
script += ` oldScript.remove();\n`;
|
||||
script += ` }\n\n`;
|
||||
script += ` const newScript = document.createElement("script");\n`;
|
||||
script += ` newScript.id = "${AppData["BunextClientHydrationScriptID"]}";\n`;
|
||||
script += ` newScript.type = "module";\n`;
|
||||
script += ` newScript.src = newScriptPath;\n`;
|
||||
// script += ` console.log("newScript", newScript);\n`;
|
||||
script += ` document.head.appendChild(newScript);\n\n`;
|
||||
script += ` } catch (err) {\n`;
|
||||
script += ` console.error("HMR update failed, falling back to reload:", err.message);\n`;
|
||||
// script += ` window.location.reload();\n`;
|
||||
script += ` }\n`;
|
||||
script += ` }\n`;
|
||||
script += ` });\n`;
|
||||
script += `});\n`;
|
||||
|
||||
return script;
|
||||
}
|
||||
|
||||
// import grabDirNames from "../../../utils/grab-dir-names";
|
||||
// import type { BundlerCTXMap, PageDistGenParams } from "../../../types";
|
||||
|
||||
// const { BUNX_HYDRATION_SRC_DIR } = grabDirNames();
|
||||
|
||||
// type Params = {
|
||||
// bundledMap?: BundlerCTXMap;
|
||||
// };
|
||||
|
||||
// export default async function ({ bundledMap }: Params) {
|
||||
// let script = "";
|
||||
|
||||
// // script += `import React from "react";\n`;
|
||||
// // script += `import { hydrateRoot } from "react-dom/client";\n`;
|
||||
// // script += `import App from "${page_file}";\n`;
|
||||
|
||||
// // script += `declare global {\n`;
|
||||
// // script += ` interface Window {\n`;
|
||||
// // script += ` ${ClientWindowPagePropsName}: any;\n`;
|
||||
// // script += ` }\n`;
|
||||
// // script += `}\n`;
|
||||
|
||||
// // script += `let root: any = null;\n\n`;
|
||||
// // script += `const component = <App {...window.${ClientWindowPagePropsName}} />;\n\n`;
|
||||
// // script += `const container = document.getElementById("${ClientRootElementIDName}");\n\n`;
|
||||
// // script += `if (container) {\n`;
|
||||
// // script += ` root = hydrateRoot(container, component);\n`;
|
||||
// // script += `}\n\n`;
|
||||
// script += `console.log(\`Development Environment\`);\n`;
|
||||
// // script += `console.log(import.meta);\n`;
|
||||
|
||||
// // script += `if (import.meta.hot) {\n`;
|
||||
// // script += ` console.log(\`HMR active\`);\n`;
|
||||
// // script += ` import.meta.hot.dispose(() => {\n`;
|
||||
// // script += ` console.log("dispose");\n`;
|
||||
// // script += ` });\n`;
|
||||
// // script += `}\n`;
|
||||
|
||||
// script += `const hmr = new EventSource("/__hmr");\n`;
|
||||
// script += `hmr.addEventListener("update", async (event) => {\n`;
|
||||
// // script += ` console.log(\`HMR even received:\`, event);\n`;
|
||||
// script += ` if (event.data) {\n`;
|
||||
// script += ` console.log(\`HMR Changes Detected. Reloading ...\`);\n`;
|
||||
// // script += ` console.log("event", event);\n`;
|
||||
// // script += ` console.log("window.${ClientRootComponentWindowName}", window.${ClientRootComponentWindowName});\n\n`;
|
||||
// // script += ` const event_data = JSON.parse(event.data);\n\n`;
|
||||
// // script += ` const new_js_path = \`/\${event_data.target_map.path}\`;\n\n`;
|
||||
|
||||
// // script += ` console.log("event_data", event_data);\n\n`;
|
||||
// // script += ` console.log("new_js_path", new_js_path);\n\n`;
|
||||
|
||||
// // script += ` if (window.${ClientRootComponentWindowName}) {\n`;
|
||||
// // script += ` const new_component = await import(new_js_path);\n`;
|
||||
// // script += ` window.${ClientRootComponentWindowName}.render(new_component);\n`;
|
||||
// // script += ` }\n`;
|
||||
|
||||
// // script += ` import("${page_file}?t=" + event.data.update).then((module) => {\n`;
|
||||
// // script += ` root.render(module.default);\n`;
|
||||
// // script += ` })\n`;
|
||||
// // script += ` console.log("root", root);\n`;
|
||||
// // script += ` root.unmount();\n`;
|
||||
// // script += ` const container = document.getElementById("${ClientRootElementIDName}");\n\n`;
|
||||
// // script += ` root = hydrateRoot(container!, component);\n`;
|
||||
// // script += ` window.history.pushState({ page: 1 }, "New Page Title", \`\${window.location.pathname}?v=\${Date.now()}\`);\n`;
|
||||
// // script += ` root.render(component);\n`;
|
||||
// script += ` window.location.reload();\n`;
|
||||
// script += ` }\n`;
|
||||
// script += ` });\n`;
|
||||
|
||||
// return script;
|
||||
// }
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
23
src/functions/server/web-pages/tailwind-esbuild-plugin.tsx
Normal file
23
src/functions/server/web-pages/tailwind-esbuild-plugin.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import * as esbuild from "esbuild";
|
||||
import postcss from "postcss";
|
||||
import tailwindcss from "@tailwindcss/postcss";
|
||||
import { readFile } from "fs/promises";
|
||||
|
||||
const tailwindEsbuildPlugin: esbuild.Plugin = {
|
||||
name: "tailwindcss",
|
||||
setup(build) {
|
||||
build.onLoad({ filter: /\.css$/ }, async (args) => {
|
||||
const source = await readFile(args.path, "utf-8");
|
||||
const result = await postcss([tailwindcss()]).process(source, {
|
||||
from: args.path,
|
||||
});
|
||||
|
||||
return {
|
||||
contents: result.css,
|
||||
loader: "css",
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default tailwindEsbuildPlugin;
|
||||
126
src/functions/server/web-pages/write-hmr-tsx-module.tsx
Normal file
126
src/functions/server/web-pages/write-hmr-tsx-module.tsx
Normal file
@ -0,0 +1,126 @@
|
||||
import * as esbuild from "esbuild";
|
||||
import tailwindEsbuildPlugin from "./tailwind-esbuild-plugin";
|
||||
import type { BundlerCTXMap } from "../../../types";
|
||||
import path from "path";
|
||||
|
||||
type Params = {
|
||||
tsx: string;
|
||||
out_file: string;
|
||||
};
|
||||
|
||||
export default async function writeHMRTsxModule({ tsx, out_file }: Params) {
|
||||
try {
|
||||
const build = await esbuild.build({
|
||||
stdin: {
|
||||
contents: tsx,
|
||||
resolveDir: process.cwd(),
|
||||
loader: "tsx",
|
||||
},
|
||||
bundle: true,
|
||||
format: "esm",
|
||||
target: "es2020",
|
||||
platform: "browser",
|
||||
external: [
|
||||
"react",
|
||||
"react-dom",
|
||||
"react/jsx-runtime",
|
||||
"react-dom/client",
|
||||
],
|
||||
minify: true,
|
||||
jsx: "automatic",
|
||||
outfile: out_file,
|
||||
plugins: [tailwindEsbuildPlugin],
|
||||
metafile: true,
|
||||
});
|
||||
|
||||
const artifacts: (
|
||||
| Pick<BundlerCTXMap, "path" | "hash" | "css_path" | "type">
|
||||
| undefined
|
||||
)[] = Object.entries(build.metafile!.outputs)
|
||||
.filter(([, meta]) => meta.entryPoint)
|
||||
.map(([outputPath, meta]) => {
|
||||
const cssPath = meta.cssBundle || undefined;
|
||||
|
||||
return {
|
||||
path: outputPath,
|
||||
hash: path.basename(outputPath, path.extname(outputPath)),
|
||||
type: outputPath.endsWith(".css")
|
||||
? "text/css"
|
||||
: "text/javascript",
|
||||
css_path: cssPath,
|
||||
};
|
||||
});
|
||||
|
||||
return artifacts?.[0];
|
||||
} catch (error) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// import * as esbuild from "esbuild";
|
||||
// import path from "path";
|
||||
// import tailwindEsbuildPlugin from "./tailwind-esbuild-plugin";
|
||||
|
||||
// const hmrExternalsPlugin: esbuild.Plugin = {
|
||||
// name: "hmr-globals",
|
||||
// setup(build) {
|
||||
// const mapping: Record<string, string> = {
|
||||
// react: "__REACT__",
|
||||
// "react-dom": "__REACT_DOM__",
|
||||
// "react-dom/client": "__REACT_DOM_CLIENT__",
|
||||
// "react/jsx-runtime": "__JSX_RUNTIME__",
|
||||
// };
|
||||
|
||||
// const filter = new RegExp(
|
||||
// `^(${Object.keys(mapping)
|
||||
// .map((k) => k.replace("/", "\\/"))
|
||||
// .join("|")})$`,
|
||||
// );
|
||||
|
||||
// build.onResolve({ filter }, (args) => {
|
||||
// return { path: args.path, namespace: "hmr-global" };
|
||||
// });
|
||||
|
||||
// build.onLoad({ filter: /.*/, namespace: "hmr-global" }, (args) => {
|
||||
// const globalName = mapping[args.path];
|
||||
// return {
|
||||
// contents: `module.exports = window.${globalName};`,
|
||||
// loader: "js",
|
||||
// };
|
||||
// });
|
||||
// },
|
||||
// };
|
||||
|
||||
// type Params = {
|
||||
// tsx: string;
|
||||
// file_path: string;
|
||||
// out_file: string;
|
||||
// };
|
||||
|
||||
// export default async function writeHMRTsxModule({
|
||||
// tsx,
|
||||
// file_path,
|
||||
// out_file,
|
||||
// }: Params) {
|
||||
// try {
|
||||
// await esbuild.build({
|
||||
// stdin: {
|
||||
// contents: tsx,
|
||||
// resolveDir: path.dirname(file_path),
|
||||
// loader: "tsx",
|
||||
// },
|
||||
// bundle: true,
|
||||
// format: "esm",
|
||||
// target: "es2020",
|
||||
// platform: "browser",
|
||||
// minify: true,
|
||||
// jsx: "automatic",
|
||||
// outfile: out_file,
|
||||
// plugins: [hmrExternalsPlugin, tailwindEsbuildPlugin],
|
||||
// });
|
||||
|
||||
// return true;
|
||||
// } catch (error) {
|
||||
// return false;
|
||||
// }
|
||||
// }
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -146,6 +146,7 @@ export type LivePageDistGenParams = {
|
||||
bundledMap?: BundlerCTXMap;
|
||||
meta?: BunextPageModuleMeta;
|
||||
routeParams?: BunxRouteParams;
|
||||
debug?: boolean;
|
||||
};
|
||||
|
||||
export type BunextPageHeadFCProps = {
|
||||
@ -244,11 +245,13 @@ export type GrabPageComponentRes = {
|
||||
module: BunextPageModule;
|
||||
meta?: BunextPageModuleMeta;
|
||||
head?: FC<BunextPageHeadFCProps>;
|
||||
debug?: boolean;
|
||||
};
|
||||
|
||||
export type GrabPageReactBundledComponentRes = {
|
||||
component: JSX.Element;
|
||||
server_res?: BunextPageModuleServerReturn;
|
||||
tsx?: string;
|
||||
};
|
||||
|
||||
export type PageFiles = {
|
||||
|
||||
@ -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`,
|
||||
),
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user