Compare commits

..

No commits in common. "60ee353bf0ecda6b699e95ba7802eb5fed787752" and "67ed8749e2ee7828e26ccf6e8e600cf6df2fa96f" have entirely different histories.

64 changed files with 715 additions and 1335 deletions

View File

@ -40,21 +40,11 @@ bunext start # Start production server from pre-built artifacts
- `src/presets/` — Default 404/500 components and sample `bunext.config.ts`
### Page module contract
Pages live in `src/pages/`. Server logic is **separated from page files** into companion `.server.ts` / `.server.tsx` files to avoid bundling server-only code into the client.
**Page file** (`page.tsx`) — client-safe exports only (bundled to the browser):
- Default export: React component receiving props from the server file
Pages live in `src/pages/`. A page file may export:
- Default export: React component receiving `ServerProps | StaticProps`
- `server`: `BunextPageServerFn` — runs server-side before rendering, return value becomes props
- `meta`: `BunextPageModuleMeta` — SEO/OG metadata
- `Head`: FC — extra `<head>` content
- `config`: `BunextRouteConfig` — cache settings
- `html_props`: `BunextHTMLProps` — attributes on the `<html>` element
**Server file** (`page.server.ts` or `page.server.tsx`) — server-only, never sent to the browser:
- Default export or `export const server`: `BunextPageServerFn` — runs server-side before rendering, return value becomes props
The framework resolves the companion by replacing the page extension with `.server.ts` or `.server.tsx`. If neither exists, no server function runs and only the default `url` prop is injected.
`__root.tsx` follows the same contract; its server companion is `__root.server.ts`.
- `head`: ReactNode — extra `<head>` content
API routes live in `src/pages/api/` and follow standard Bun `Request → Response` handler conventions.

View File

@ -200,15 +200,12 @@ my-app/
├── src/
│ └── pages/ # File-system routes (pages and API handlers)
│ ├── __root.tsx # Optional: root layout wrapper for all pages
│ ├── __root.server.ts # Optional: root-level server logic
│ ├── index.tsx # Route: /
│ ├── index.server.ts # Optional: server logic for index route
│ ├── about.tsx # Route: /about
│ ├── 404.tsx # Optional: custom 404 page
│ ├── 500.tsx # Optional: custom 500 page
│ ├── blog/
│ │ ├── index.tsx # Route: /blog
│ │ ├── index.server.ts # Server logic for /blog
│ │ └── [slug].tsx # Route: /blog/:slug (dynamic)
│ └── api/
│ └── users.ts # API route: /api/users
@ -278,15 +275,19 @@ export default function HomePage() {
### Server Function
Server logic lives in a companion **`.server.ts`** (or `.server.tsx`) file alongside the page. The framework looks for `<page>.server.ts` or `<page>.server.tsx` next to the page file and loads it separately on the server — it is never bundled into the client JS.
Export a `server` function to run server-side logic before rendering. The return value's `props` field is spread into the page component as props, and `query` carries route query parameters.
The server file exports the server function as either `export default` or `export const server`. The return value's `props` field is spread into the page component as props, and `query` carries route query parameters.
```ts
// src/pages/profile.server.ts
```tsx
// src/pages/profile.tsx
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
const server: BunextPageServerFn<{
type Props = {
props?: { username: string; bio: string };
query?: Record<string, string>;
url?: URL;
};
export const server: BunextPageServerFn<{
username: string;
bio: string;
}> = async (ctx) => {
@ -303,17 +304,6 @@ const server: BunextPageServerFn<{
};
};
export default server;
```
```tsx
// src/pages/profile.tsx (client-only exports — bundled to the browser)
type Props = {
props?: { username: string; bio: string };
query?: Record<string, string>;
url?: URL;
};
export default function ProfilePage({ props, url }: Props) {
return (
<div>
@ -325,8 +315,6 @@ export default function ProfilePage({ props, url }: Props) {
}
```
> **Why separate files?** Bundling server code (Bun APIs, database clients, `fs`, secrets) into the same file as a React component causes TypeScript compilation errors because the bundler processes the page file for the browser. The `.server.ts` companion file is loaded only by the server at request time and is never included in the client bundle.
The server function receives a `ctx` object (type `BunxRouteParams`) with:
| Field | Type | Description |
@ -378,13 +366,10 @@ The `url` prop exposes the following fields from the standard Web `URL` interfac
### Redirects from Server
Return a `redirect` object from the server function to redirect the client:
Return a `redirect` object from the `server` function to redirect the client:
```ts
// src/pages/dashboard.server.ts
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
const server: BunextPageServerFn = async (ctx) => {
```tsx
export const server: BunextPageServerFn = async (ctx) => {
const isLoggedIn = false; // check auth
if (!isLoggedIn) {
@ -399,8 +384,6 @@ const server: BunextPageServerFn = async (ctx) => {
return { props: {} };
};
export default server;
```
`permanent: true` sends a `301` redirect. Otherwise it sends `302`, or the value of `status_code` if provided.
@ -409,11 +392,8 @@ export default server;
Control status codes, headers, and other response options from the server function:
```ts
// src/pages/submit.server.ts
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
const server: BunextPageServerFn = async (ctx) => {
```tsx
export const server: BunextPageServerFn = async (ctx) => {
return {
props: { message: "Created" },
responseOptions: {
@ -424,13 +404,11 @@ const server: BunextPageServerFn = async (ctx) => {
},
};
};
export default server;
```
### SEO Metadata
Export a `meta` object from the **page file** (not the server file) to inject SEO and Open Graph tags into the `<head>`:
Export a `meta` object to inject SEO and Open Graph tags into the `<head>`:
```tsx
import type { BunextPageModuleMeta } from "@moduletrace/bunext/types";
@ -469,7 +447,7 @@ export default function AboutPage() {
### Dynamic Metadata
`meta` can also be an async function that receives the request context and server response. Like `meta`, it is exported from the **page file**:
`meta` can also be an async function that receives the request context and server response:
```tsx
import type { BunextPageModuleMetaFn } from "@moduletrace/bunext/types";
@ -505,21 +483,7 @@ export default function Page() {
### Root Layout
Create `src/pages/__root.tsx` to wrap every page in a shared layout. The root component receives `children` (the current page component) along with all server props.
If the root layout also needs server-side logic, place it in `src/pages/__root.server.ts` (or `.server.tsx`) — the same `.server.*` convention used by regular pages:
```ts
// src/pages/__root.server.ts
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
const server: BunextPageServerFn = async (ctx) => {
// e.g. fetch navigation links, check auth
return { props: { navLinks: ["/", "/about"] } };
};
export default server;
```
Create `src/pages/__root.tsx` to wrap every page in a shared layout. The root component receives `children` (the current page component) along with all server props:
```tsx
// src/pages/__root.tsx
@ -672,13 +636,12 @@ export default function ProductsPage() {
### Dynamic Cache Control from Server Function
Cache settings can also be returned from the server function, which lets you conditionally enable caching based on request data:
Cache settings can also be returned from the `server` function, which lets you conditionally enable caching based on request data:
```ts
// src/pages/products.server.ts
```tsx
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
const server: BunextPageServerFn = async (ctx) => {
export const server: BunextPageServerFn = async (ctx) => {
const data = await fetchProducts();
return {
@ -688,11 +651,6 @@ const server: BunextPageServerFn = async (ctx) => {
};
};
export default server;
```
```tsx
// src/pages/products.tsx
export default function ProductsPage({ props }: any) {
return (
<ul>
@ -999,7 +957,7 @@ Request
1. Match route via FileSystemRouter
2. Find bundled artifact in BUNDLER_CTX_MAP
3. Import page module (with cache-busting timestamp in dev)
4. Import companion server module (<page>.server.ts/tsx) if it exists; run its exported function for server-side data
4. Run module.server(ctx) for server-side data
5. Resolve meta (static object or async function)
6. renderToString(component) → inject into HTML template
7. Inject window.__PAGE_PROPS__, hydration <script>, CSS <link>

View File

@ -58,7 +58,7 @@ This report compares the two on their overlapping surface — server-side render
| Router | `Bun.FileSystemRouter` | Custom (Pages Router) / React Router (App Router) |
| SSR method | `renderToString` (complete response, by design) | `renderToReadableStream` (streaming) |
| Component model | Classic SSR + hydration | React Server Components + Client Components |
| Data fetching | Per-page `.server.ts` companion file | `getServerSideProps`, `getStaticProps`, `fetch` in RSC |
| Data fetching | Per-page `server` export | `getServerSideProps`, `getStaticProps`, `fetch` in RSC |
| State persistence | `window.__PAGE_PROPS__` | RSC payload, router cache |
| Dev HMR transport | Server-Sent Events (SSE) | WebSocket |
| Config format | `bunext.config.ts` | `next.config.js` / `next.config.ts` |
@ -210,25 +210,19 @@ Streaming SSR's benefits — progressive flushing, Suspense-based partial render
### Data Fetching
**Bunext** exposes a single data-fetching primitive: a companion **`.server.ts`** file alongside each page.
**Bunext** exposes a single data-fetching primitive: the `server` export on each page module.
```ts
// src/pages/products.server.ts
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
const server: BunextPageServerFn = async (ctx) => {
export const server: BunextPageServerFn = async (ctx) => {
const data = await db.query(...);
return { props: { data } };
};
export default server;
```
The server file is never bundled into client JS — it runs exclusively on the server at request time. The return value is serialized to `window.__PAGE_PROPS__` and passed as component props. A `url` object (copy of the request `URL`) is **always** injected into server props as a default, so every page can read URL metadata without writing a server file at all.
The return value is serialized to `window.__PAGE_PROPS__` and passed as component props. A `url` object (copy of the request `URL`) is **always** injected into server props as a default, so every page can read URL metadata without writing a server function.
**Design notes:**
- One server file per page. Data fetching is centralised at the page level, not scattered across components.
- The page file (`.tsx`) exports only client-safe code — the React component, `meta`, `Head`, `config`, and `html_props`. Server-only code (Bun APIs, database clients, `fs`) lives in the `.server.ts` companion and is never sent to the browser.
- One server function per page. Data fetching is centralised at the page level, not scattered across components.
- All rendering is on-demand. SSG is intentionally out of scope — see [Caching](#caching) for how Bunext addresses this differently.
- Server function result is passed via `window.__PAGE_PROPS__`, serialized to JSON and embedded in the HTML — large payloads increase page size.
@ -295,7 +289,7 @@ Bunext's caching model is its answer to SSG. Rather than pre-building pages at d
**Bunext** implements a **file-based HTML cache**:
- Enabled per-page via `config.cachePage` (exported from the page file) or returned dynamically from the server function in the `.server.ts` companion at runtime.
- Enabled per-page via `config.cachePage` or returned dynamically from the `server` function at runtime.
- On a cache miss, the rendered HTML is written to `public/__bunext/cache/<key>.res.html` alongside a metadata file `<key>.meta.json` (creation timestamp, expiry, paradigm).
- On a cache hit, the HTML file is read and returned with `X-Bunext-Cache: HIT`.
- A cron job runs every 30 seconds to delete expired entries.
@ -322,7 +316,7 @@ The key distinction from SSG: Bunext's cache is **demand-driven**. A site with 1
### Metadata and SEO
**Bunext** supports both static and dynamic metadata. These exports live in the **page file** (not the `.server.ts` companion), since they are processed at the server HTML-generation step and may reference types from the client module:
**Bunext** supports both static and dynamic metadata:
- `export const meta: BunextPageModuleMeta` — static object with `title`, `description`, `keywords`, `author`, `robots`, `canonical`, `themeColor`, `og.*`, and `twitter.*` fields.
- `export const meta: BunextPageModuleMetaFn` — async function receiving `ctx` and `serverRes` for dynamic metadata based on fetched data.
@ -546,13 +540,10 @@ Next.js wraps these in `NextRequest` and `NextResponse`, which add convenience m
### Conditional Runtime Caching
Bunext's server function can return `cachePage: true` and `cacheExpiry: N` based on runtime data — the authenticated state of the user, A/B test bucket, content freshness, or any other request-time condition:
Bunext's `server` function can return `cachePage: true` and `cacheExpiry: N` based on runtime data — the authenticated state of the user, A/B test bucket, content freshness, or any other request-time condition:
```ts
// src/pages/products.server.ts
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
const server: BunextPageServerFn = async (ctx) => {
export const server: BunextPageServerFn = async (ctx) => {
const user = await getUser(ctx.req);
return {
props: { data },
@ -560,21 +551,16 @@ const server: BunextPageServerFn = async (ctx) => {
cacheExpiry: 300,
};
};
export default server;
```
Next.js's ISR and full-route cache operate on fixed revalidation intervals set at build time. There is no mechanism to decide at runtime whether a specific request should be cached.
### Page Response Transform
The `resTransform` field in the server function's `ctx` parameter lets the developer post-process the final HTML response generated by the framework — add headers, set cookies, modify status codes — without touching middleware:
The `resTransform` field in the page `server` function's `ctx` parameter lets the developer post-process the final HTML response generated by the framework — add headers, set cookies, modify status codes — without touching middleware:
```ts
// src/pages/page.server.ts
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
const server: BunextPageServerFn = async (ctx) => {
export const server: BunextPageServerFn = async (ctx) => {
ctx.resTransform = (res) => {
res.headers.set("X-Custom-Header", "value");
return res;
@ -582,8 +568,6 @@ const server: BunextPageServerFn = async (ctx) => {
return { props: {} };
};
export default server;
```
This exists because page responses are generated entirely by the framework (`renderToString` → HTML template). Unlike API routes — where the developer returns a `Response` directly and already has full control — there is no other hook to modify the final page response at the route level without going through global middleware.
@ -604,7 +588,7 @@ Next.js's codebase spans Turbopack (Rust), SWC (Rust), the App Router internals,
The RSC model requires developers to constantly reason about the `"use client"` / `"use server"` boundary: what can be async, what has access to browser APIs, what gets serialized into the RSC payload. Mistakes at this boundary produce runtime errors that are difficult to diagnose.
Bunext has one rule: the `.server.ts` companion file runs on the server, the page file runs on both (SSR then hydration). The separation is enforced by the file system, not by decorators or directives. There is no boundary to reason about inside a file.
Bunext has one rule: the `server` function runs on the server, the component runs on both (SSR then hydration). There is no boundary to reason about.
### No Vendor Lock-In

View File

@ -1,23 +1,23 @@
import { Command } from "commander";
import { log } from "../../utils/log";
import init from "../../functions/init";
// import rewritePagesModule from "../../utils/rewrite-pages-module";
import rewritePagesModule from "../../utils/rewrite-pages-module";
import allPagesBunBundler from "../../functions/bundler/all-pages-bun-bundler";
import grabDirNames from "../../utils/grab-dir-names";
import { rmSync } from "fs";
import allPagesBundler from "../../functions/bundler/all-pages-bundler";
const { HYDRATION_DST_DIR, BUNX_CWD_PAGES_REWRITE_DIR } = grabDirNames();
export default function () {
return new Command("build")
.description("Build Project")
.action(async () => {
process.env.NODE_ENV = "production";
process.env.BUILD = "true";
try {
rmSync(HYDRATION_DST_DIR, { recursive: true });
rmSync(BUNX_CWD_PAGES_REWRITE_DIR, { recursive: true });
}
catch (error) { }
global.SKIPPED_BROWSER_MODULES = new Set();
// await rewritePagesModule();
await rewritePagesModule();
await init();
log.banner();
log.build("Building Project ...");

View File

@ -2,9 +2,9 @@ import { Command } from "commander";
import startServer from "../../functions/server/start-server";
import { log } from "../../utils/log";
import bunextInit from "../../functions/bunext-init";
import rewritePagesModule from "../../utils/rewrite-pages-module";
import grabDirNames from "../../utils/grab-dir-names";
import { rmSync } from "fs";
import allPagesBunBundler from "../../functions/bundler/all-pages-bun-bundler";
const { HYDRATION_DST_DIR, BUNX_CWD_PAGES_REWRITE_DIR } = grabDirNames();
export default function () {
return new Command("dev")
@ -17,8 +17,8 @@ export default function () {
rmSync(BUNX_CWD_PAGES_REWRITE_DIR, { recursive: true });
}
catch (error) { }
await rewritePagesModule();
await bunextInit();
await allPagesBunBundler();
await startServer();
});
}

View File

@ -2,14 +2,13 @@ import { Command } from "commander";
import startServer from "../../functions/server/start-server";
import { log } from "../../utils/log";
import bunextInit from "../../functions/bunext-init";
import allPagesBunBundler from "../../functions/bundler/all-pages-bun-bundler";
export default function () {
return new Command("start")
.description("Start production server")
.action(async () => {
process.env.NODE_ENV = "production";
log.info("Starting production server ...");
await bunextInit();
await allPagesBunBundler();
await startServer();
});
}

View File

@ -7,7 +7,6 @@ import path from "path";
import grabClientHydrationScript from "./grab-client-hydration-script";
import { mkdirSync, rmSync } from "fs";
import recordArtifacts from "./record-artifacts";
import BunSkipNonBrowserPlugin from "./plugins/bun-skip-browser-plugin";
const { HYDRATION_DST_DIR, BUNX_HYDRATION_SRC_DIR, BUNX_TMP_DIR } = grabDirNames();
export default async function allPagesBunBundler(params) {
const { target = "browser", page_file_paths } = params || {};
@ -38,16 +37,15 @@ export default async function allPagesBunBundler(params) {
if (entryToPage.size === 0)
return;
const buildStart = performance.now();
const define = {
"process.env.NODE_ENV": JSON.stringify(dev ? "development" : "production"),
};
const result = await Bun.build({
entrypoints: [...entryToPage.keys()],
outdir: HYDRATION_DST_DIR,
root: BUNX_HYDRATION_SRC_DIR,
minify: !dev,
minify: true,
format: "esm",
define,
define: {
"process.env.NODE_ENV": JSON.stringify(dev ? "development" : "production"),
},
naming: {
entry: "[dir]/[hash].[ext]",
chunk: "chunks/[hash].[ext]",
@ -96,10 +94,7 @@ export default async function allPagesBunBundler(params) {
});
}
if (artifacts?.[0]) {
await recordArtifacts({
artifacts,
page_file_paths,
});
await recordArtifacts({ artifacts });
}
const elapsed = (performance.now() - buildStart).toFixed(0);
log.success(`[Built] in ${elapsed}ms`);

View File

@ -8,7 +8,6 @@ import grabClientHydrationScript from "./grab-client-hydration-script";
import grabArtifactsFromBundledResults from "./grab-artifacts-from-bundled-result";
import { writeFileSync } from "fs";
import recordArtifacts from "./record-artifacts";
import stripServerSideLogic from "./strip-server-side-logic";
const { HYDRATION_DST_DIR, HYDRATION_DST_DIR_MAP_JSON_FILE } = grabDirNames();
let build_starts = 0;
const MAX_BUILD_STARTS = 10;
@ -24,7 +23,7 @@ export default async function allPagesBundler(params) {
const virtualEntries = {};
const dev = isDevelopment();
for (const page of target_pages) {
const key = page.local_path;
const key = page.transformed_path;
const txt = await grabClientHydrationScript({
page_local_path: page.local_path,
});
@ -33,11 +32,6 @@ export default async function allPagesBundler(params) {
// }
if (!txt)
continue;
// const final_tsx = stripServerSideLogic({
// txt_code: txt,
// file_path: key,
// });
// console.log("final_tsx", final_tsx);
virtualEntries[key] = txt;
}
const virtualPlugin = {
@ -72,28 +66,6 @@ export default async function allPagesBundler(params) {
},
};
const entryPoints = Object.keys(virtualEntries).map((k) => `virtual:${k}`);
// let alias: any = {};
// const excludes = [
// "bun:sqlite",
// "path",
// "url",
// "events",
// "util",
// "crypto",
// "net",
// "tls",
// "fs",
// "node:path",
// "node:url",
// "node:process",
// "node:fs",
// "node:timers/promises",
// ];
// for (let i = 0; i < excludes.length; i++) {
// const exclude = excludes[i];
// alias[exclude] = "./empty.js";
// }
// console.log("alias", alias);
const result = await esbuild.build({
entryPoints,
outdir: HYDRATION_DST_DIR,
@ -117,7 +89,6 @@ export default async function allPagesBundler(params) {
"react-dom/client",
"react/jsx-runtime",
],
// alias,
});
if (result.errors.length > 0) {
for (const error of result.errors) {

View File

@ -4,27 +4,24 @@ import grabDirNames from "../../utils/grab-dir-names";
import AppNames from "../../utils/grab-app-names";
import grabConstants from "../../utils/grab-constants";
import pagePathTransform from "../../utils/page-path-transform";
import grabRootFilePath from "../server/web-pages/grab-root-file-path";
const { PAGES_DIR } = grabDirNames();
export default async function grabClientHydrationScript({ page_local_path, }) {
const { ClientRootElementIDName, ClientRootComponentWindowName, ClientWindowPagePropsName, } = grabConstants();
const { root_file_path } = grabRootFilePath();
// const target_path = pagePathTransform({ page_path: page_local_path });
// const target_root_path = root_file_path
// ? pagePathTransform({ page_path: root_file_path })
// : undefined;
const target_path = pagePathTransform({ page_path: page_local_path });
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 (root_file_path) {
txt += `import Root from "${root_file_path}";\n`;
if (does_root_exist) {
txt += `import Root from "${root_component_path}";\n`;
}
txt += `import Page from "${page_local_path}";\n\n`;
txt += `import Page from "${target_path}";\n\n`;
txt += `const pageProps = window.${ClientWindowPagePropsName} || {};\n`;
if (root_file_path) {
txt += `const component = <Root {...pageProps}><Page {...pageProps} /></Root>\n`;
if (does_root_exist) {
txt += `const component = <Root suppressHydrationWarning={true} {...pageProps}><Page {...pageProps} /></Root>\n`;
}
else {
txt += `const component = <Page {...pageProps} />\n`;
txt += `const component = <Page suppressHydrationWarning={true} {...pageProps} />\n`;
}
txt += `if (window.${ClientRootComponentWindowName}?.render) {\n`;
txt += ` window.${ClientRootComponentWindowName}.render(component);\n`;

View File

@ -1,75 +1,31 @@
import { log } from "../../../utils/log";
const BunSkipNonBrowserPlugin = {
name: "skip-non-browser",
setup(build) {
const skipFilter = /^(bun:|node:|fs$|path$|os$|crypto$|net$|events$|util$|tls$|url$|process$)/;
// const skipped_modules = new Set<string>();
build.onResolve({ filter: skipFilter }, (args) => {
global.SKIPPED_BROWSER_MODULES.add(args.path);
return {
path: args.path,
namespace: "skipped",
// external: true,
};
build.onResolve({ filter: /^(bun:|node:)/ }, (args) => {
return { path: args.path, external: true };
});
// build.onEnd(() => {
// log.warn(`global.SKIPPED_BROWSER_MODULES`, [
// ...global.SKIPPED_BROWSER_MODULES,
// ]);
// });
// build.onResolve({ filter: /^[^./]/ }, (args) => {
// // If it's a built-in like 'fs' or 'path', skip it immediately
// const excludes = [
// "fs",
// "path",
// "os",
// "crypto",
// "net",
// "events",
// "util",
// "tls",
// ];
// if (excludes.includes(args.path) || args.path.startsWith("node:")) {
// return {
// path: args.path,
// // namespace: "skipped",
// external: true,
// };
// }
// try {
// Bun.resolveSync(args.path, args.importer || process.cwd());
// return null;
// } catch (e) {
// console.warn(`[Skip] Mark as external: ${args.path}`);
// return {
// path: args.path,
// // namespace: "skipped",
// external: true,
// };
// }
// });
build.onLoad({ filter: /.*/, namespace: "skipped" }, (args) => {
return {
contents: `
const proxy = new Proxy(() => proxy, {
get: () => proxy,
construct: () => proxy,
});
export const Database = proxy;
export const join = proxy;
export const fileURLToPath = proxy;
export const arch = proxy;
export const platform = proxy;
export const statSync = proxy;
export const $H = proxy;
export const _ = proxy;
export default proxy;
`,
loader: "js",
};
build.onResolve({ filter: /^[^./]/ }, (args) => {
// If it's a built-in like 'fs' or 'path', skip it immediately
const excludes = [
"fs",
"path",
"os",
"crypto",
"net",
"events",
"util",
];
if (excludes.includes(args.path) || args.path.startsWith("node:")) {
return { path: args.path, external: true };
}
try {
Bun.resolveSync(args.path, args.importer || process.cwd());
return null;
}
catch (e) {
console.warn(`[Skip] Mark as external: ${args.path}`);
return { path: args.path, external: true };
}
});
},
};

View File

@ -1,7 +1,6 @@
import type { BundlerCTXMap } from "../../types";
type Params = {
artifacts: BundlerCTXMap[];
page_file_paths?: string[];
};
export default function recordArtifacts({ artifacts, page_file_paths, }: Params): Promise<void>;
export default function recordArtifacts({ artifacts }: Params): Promise<void>;
export {};

View File

@ -1,7 +1,6 @@
import grabDirNames from "../../utils/grab-dir-names";
import _ from "lodash";
const { HYDRATION_DST_DIR_MAP_JSON_FILE } = grabDirNames();
export default async function recordArtifacts({ artifacts, page_file_paths, }) {
export default async function recordArtifacts({ artifacts }) {
const artifacts_map = {};
for (const artifact of artifacts) {
if (artifact?.local_path) {
@ -9,10 +8,7 @@ export default async function recordArtifacts({ artifacts, page_file_paths, }) {
}
}
if (global.BUNDLER_CTX_MAP) {
global.BUNDLER_CTX_MAP = _.merge(global.BUNDLER_CTX_MAP, artifacts_map);
global.BUNDLER_CTX_MAP = artifacts_map;
}
// await Bun.write(
// HYDRATION_DST_DIR_MAP_JSON_FILE,
// JSON.stringify(artifacts_map, null, 4),
// );
await Bun.write(HYDRATION_DST_DIR_MAP_JSON_FILE, JSON.stringify(artifacts_map, null, 4));
}

View File

@ -22,6 +22,5 @@ declare global {
var CURRENT_VERSION: string | undefined;
var PAGE_FILES: PageFiles[];
var ROOT_FILE_UPDATED: boolean;
var SKIPPED_BROWSER_MODULES: Set<string>;
}
export default function bunextInit(): Promise<void>;

View File

@ -1,11 +1,13 @@
import ora, {} from "ora";
import grabDirNames from "../utils/grab-dir-names";
import {} from "fs";
import { readFileSync } from "fs";
import init from "./init";
import isDevelopment from "../utils/is-development";
import allPagesBundler from "./bundler/all-pages-bundler";
import watcher from "./server/watcher";
import { log } from "../utils/log";
import cron from "./server/cron";
import EJSON from "../utils/ejson";
import allPagesBunBundler from "./bundler/all-pages-bun-bundler";
const { PAGES_DIR, HYDRATION_DST_DIR_MAP_JSON_FILE } = grabDirNames();
export default async function bunextInit() {
@ -15,7 +17,6 @@ export default async function bunextInit() {
global.BUNDLER_CTX_MAP = {};
global.BUNDLER_REBUILDS = 0;
global.PAGE_FILES = [];
global.SKIPPED_BROWSER_MODULES = new Set();
await init();
log.banner();
const router = new Bun.FileSystemRouter({
@ -25,9 +26,17 @@ export default async function bunextInit() {
global.ROUTER = router;
const is_dev = isDevelopment();
if (is_dev) {
// await allPagesBundler();
await allPagesBunBundler();
watcher();
}
else {
const artifacts = EJSON.parse(readFileSync(HYDRATION_DST_DIR_MAP_JSON_FILE, "utf-8"));
if (!artifacts) {
log.error("Please build first.");
process.exit(1);
}
global.BUNDLER_CTX_MAP = artifacts;
cron();
}
}

View File

@ -3,7 +3,7 @@ import path from "path";
import grabDirNames from "../../utils/grab-dir-names";
import rebuildBundler from "./rebuild-bundler";
import { log } from "../../utils/log";
// import rewritePagesModule from "../../utils/rewrite-pages-module";
import rewritePagesModule from "../../utils/rewrite-pages-module";
const { ROOT_DIR } = grabDirNames();
export default async function watcher() {
const pages_src_watcher = watch(ROOT_DIR, {
@ -62,15 +62,14 @@ async function fullRebuild(params) {
try {
const { msg } = params || {};
global.RECOMPILING = true;
// const target_file_paths = global.HMR_CONTROLLERS.map(
// (hmr) => hmr.target_map?.local_path,
// ).filter((f) => typeof f == "string");
// await rewritePagesModule();
const target_file_paths = global.HMR_CONTROLLERS.map((hmr) => hmr.target_map?.local_path).filter((f) => typeof f == "string");
await rewritePagesModule({
page_file_path: target_file_paths,
});
if (msg) {
log.watch(msg);
}
await rebuildBundler();
// await rebuildBundler({ target_file_paths });
await rebuildBundler({ target_file_paths });
}
catch (error) {
log.error(error);

View File

@ -1,2 +1,2 @@
import type { LivePageDistGenParams } from "../../../types";
export default function genWebHTML({ component, pageProps, bundledMap, module, routeParams, debug, root_module, }: LivePageDistGenParams): Promise<string>;
export default function genWebHTML({ component, pageProps, bundledMap, head: Head, module, meta, routeParams, debug, }: LivePageDistGenParams): Promise<string>;

View File

@ -1,4 +1,4 @@
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
import { jsx as _jsx } from "react/jsx-runtime";
import { renderToString } from "react-dom/server";
import grabContants from "../../../utils/grab-constants";
import EJSON from "../../../utils/ejson";
@ -14,68 +14,56 @@ try {
_reactVersion = JSON.parse(readFileSync(path.join(process.cwd(), "node_modules/react/package.json"), "utf-8")).version;
}
catch { }
export default async function genWebHTML({ component, pageProps, bundledMap, module, routeParams, debug, root_module, }) {
export default async function genWebHTML({ component, pageProps, bundledMap, head: Head, module, meta, routeParams, debug, }) {
const { ClientRootElementIDName, ClientWindowPagePropsName } = grabContants();
const is_dev = isDevelopment();
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 }))
: "";
let html = `<!DOCTYPE html>\n`;
html += `<html>\n`;
html += ` <head>\n`;
html += ` <meta charset="utf-8" />\n`;
html += ` <meta name="viewport" content="width=device-width, initial-scale=1.0">\n`;
if (meta) {
html += ` ${grabWebMetaHTML({ meta })}\n`;
}
if (bundledMap?.css_path) {
html += ` <link rel="stylesheet" href="/${bundledMap.css_path}" />\n`;
}
const serializedProps = (EJSON.stringify(pageProps || {}) || "{}").replace(/<\//g, "<\\/");
const page_hydration_script = await grabWebPageHydrationScript();
const root_meta = root_module?.meta
? typeof root_module.meta == "function" && routeParams
? await root_module.meta({ ctx: routeParams, serverRes: pageProps })
: typeof root_module.meta == "function"
? undefined
: root_module.meta
: undefined;
const page_meta = module?.meta
? typeof module.meta == "function" && routeParams
? await module.meta({ ctx: routeParams, serverRes: pageProps })
: typeof module.meta == "function"
? undefined
: module.meta
: undefined;
const html_props = {
...module?.html_props,
...root_module?.html_props,
};
const Head = module?.Head;
const RootHead = root_module?.Head;
html += ` <script>window.${ClientWindowPagePropsName} = ${serializedProps}</script>\n`;
if (bundledMap?.path) {
const dev = isDevelopment();
const devSuffix = dev ? "?dev" : "";
const browser_imports = {
react: `https://esm.sh/react@${_reactVersion}`,
"react-dom": `https://esm.sh/react-dom@${_reactVersion}`,
"react-dom/client": `https://esm.sh/react-dom@${_reactVersion}/client`,
"react/jsx-runtime": `https://esm.sh/react@${_reactVersion}/jsx-runtime`,
};
if (dev) {
browser_imports["react/jsx-dev-runtime"] =
`https://esm.sh/react@${_reactVersion}/jsx-dev-runtime`;
}
const importMap = JSON.stringify({
imports: browser_imports,
imports: {
react: `https://esm.sh/react@${_reactVersion}${devSuffix}`,
"react-dom": `https://esm.sh/react-dom@${_reactVersion}${devSuffix}`,
"react-dom/client": `https://esm.sh/react-dom@${_reactVersion}/client${devSuffix}`,
"react/jsx-runtime": `https://esm.sh/react@${_reactVersion}/jsx-runtime${devSuffix}`,
"react/jsx-dev-runtime": `https://esm.sh/react@${_reactVersion}/jsx-dev-runtime${devSuffix}`,
},
});
let final_component = (_jsxs("html", { ...html_props, children: [_jsxs("head", { children: [_jsx("meta", { charSet: "utf-8" }), _jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1.0" }), root_meta ? grabWebMetaHTML({ meta: root_meta }) : null, page_meta ? grabWebMetaHTML({ meta: page_meta }) : null, bundledMap?.css_path ? (_jsx("link", { rel: "stylesheet", href: `/${bundledMap.css_path}` })) : null, _jsx("script", { dangerouslySetInnerHTML: {
__html: `window.${ClientWindowPagePropsName} = ${serializedProps}`,
} }), bundledMap?.path ? (_jsxs(_Fragment, { children: [_jsx("script", { type: "importmap", dangerouslySetInnerHTML: {
__html: importMap,
}, fetchPriority: "high" }), _jsx("script", { src: `/${bundledMap.path}`, type: "module", id: AppData["BunextClientHydrationScriptID"], defer: true })] })) : null, is_dev ? (_jsx("script", { defer: true, dangerouslySetInnerHTML: {
__html: page_hydration_script,
} })) : null, RootHead ? (_jsx(RootHead, { serverRes: pageProps, ctx: routeParams })) : null, Head ? _jsx(Head, { serverRes: pageProps, ctx: routeParams }) : null] }), _jsx("body", { children: _jsx("div", { id: ClientRootElementIDName, suppressHydrationWarning: !dev, children: component }) })] }));
let html = `<!DOCTYPE html>\n`;
// const stream = await renderToReadableStream(final_component, {
// onError(error: any) {
// // This is where you "omit" or handle the errors
// // You can log it silently or ignore it
// if (error.message.includes('unique "key" prop')) return;
// console.error(error);
// },
// });
// // 2. Convert the Web Stream to a String (Bun-optimized)
// const htmlBody = await new Response(stream).text();
// html += htmlBody;
html += renderToString(final_component);
html += ` <script type="importmap">${importMap}</script>\n`;
html += ` <script src="/${bundledMap.path}" type="module" id="${AppData["BunextClientHydrationScriptID"]}" async></script>\n`;
}
if (isDevelopment()) {
html += `<script defer>\n${await grabWebPageHydrationScript()}\n</script>\n`;
}
if (headHTML) {
html += ` ${headHTML}\n`;
}
html += ` </head>\n`;
html += ` <body>\n`;
html += ` <div id="${ClientRootElementIDName}">${componentHTML}</div>\n`;
html += ` </body>\n`;
html += `</html>\n`;
return html;
}

View File

@ -1,2 +1,2 @@
import type { GrabPageComponentRes } from "../../../types";
export default function generateWebPageResponseFromComponentReturn({ component, module, bundledMap, routeParams, serverRes, debug, root_module, }: GrabPageComponentRes): Promise<Response>;
export default function generateWebPageResponseFromComponentReturn({ component, module, bundledMap, head, meta, routeParams, serverRes, debug, }: GrabPageComponentRes): Promise<Response>;

View File

@ -1,17 +1,17 @@
import _ from "lodash";
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, routeParams, serverRes, debug, root_module, }) {
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,
root_module,
});
if (debug) {
log.info("html", html);
@ -36,9 +36,8 @@ export default async function generateWebPageResponseFromComponentReturn({ compo
Expires: "0",
};
}
const config = _.merge(root_module?.config, module.config);
const cache_page = config?.cachePage || serverRes?.cachePage || false;
const expiry_seconds = config?.cacheExpiry || serverRes?.cacheExpiry;
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({

View File

@ -4,8 +4,6 @@ import grabPageBundledReactComponent from "./grab-page-bundled-react-component";
import _ from "lodash";
import { log } from "../../../utils/log";
import grabRootFilePath from "./grab-root-file-path";
import grabPageServerRes from "./grab-page-server-res";
import grabPageServerPath from "./grab-page-server-path";
class NotFoundError extends Error {
}
export default async function grabPageComponent({ req, file_path: passed_file_path, debug, }) {
@ -37,7 +35,6 @@ export default async function grabPageComponent({ req, file_path: passed_file_pa
}
const bundledMap = global.BUNDLER_CTX_MAP?.[file_path];
if (!bundledMap?.path) {
console.log(global.BUNDLER_CTX_MAP);
const errMsg = `No Bundled File Path for this request path!`;
log.error(errMsg);
throw new Error(errMsg);
@ -46,52 +43,72 @@ export default async function grabPageComponent({ req, file_path: passed_file_pa
log.info(`bundledMap:`, bundledMap);
}
const { root_file_path } = grabRootFilePath();
const root_module = root_file_path
? await import(`${root_file_path}?t=${now}`)
: undefined;
const { server_file_path: root_server_file_path } = root_file_path
? grabPageServerPath({ file_path: root_file_path })
: {};
const root_server_module = root_server_file_path
? await import(`${root_server_file_path}?t=${now}`)
: undefined;
const root_server_fn = root_server_module?.default || root_server_module?.server;
const rootServerRes = root_server_fn
? await grabPageServerRes({
server_function: root_server_fn,
url,
query: match?.query,
routeParams,
})
: undefined;
if (debug) {
log.info(`rootServerRes:`, rootServerRes);
}
const module = await import(`${file_path}?t=${now}`);
const { server_file_path } = grabPageServerPath({ file_path });
const server_module = server_file_path
? await import(`${server_file_path}?t=${now}`)
: undefined;
if (debug) {
log.info(`module:`, module);
}
const server_fn = server_module?.default || server_module?.server;
const serverRes = server_fn
? await grabPageServerRes({
server_function: server_fn,
url,
const serverRes = await (async () => {
const default_props = {
url: {
..._.pick(url, [
"host",
"hostname",
"pathname",
"origin",
"port",
"search",
"searchParams",
"hash",
"href",
"password",
"protocol",
"username",
]),
},
query: match?.query,
routeParams,
})
: undefined;
};
try {
if (routeParams) {
const serverData = await module["server"]?.({
...routeParams,
query: { ...routeParams.query, ...match?.query },
});
return {
...serverData,
...default_props,
};
}
return {
...default_props,
};
}
catch (error) {
return {
...default_props,
};
}
})();
if (debug) {
log.info(`serverRes:`, serverRes);
}
const mergedServerRes = _.merge(rootServerRes || {}, serverRes || {});
const meta = module.meta
? typeof module.meta == "function" && routeParams
? await module.meta({
ctx: routeParams,
serverRes,
})
: typeof module.meta == "object"
? module.meta
: undefined
: undefined;
if (debug) {
log.info(`meta:`, meta);
}
const Head = module.Head;
const { component } = (await grabPageBundledReactComponent({
file_path,
root_file_path,
server_res: mergedServerRes,
server_res: serverRes,
})) || {};
if (!component) {
throw new Error(`Couldn't grab page component`);
@ -101,11 +118,12 @@ export default async function grabPageComponent({ req, file_path: passed_file_pa
}
return {
component,
serverRes: mergedServerRes,
serverRes,
routeParams,
module,
bundledMap,
root_module,
meta,
head: Head,
};
}
catch (error) {

View File

@ -2,29 +2,21 @@ import EJSON from "../../../utils/ejson";
import pagePathTransform from "../../../utils/page-path-transform";
export default function grabPageReactComponentString({ file_path, root_file_path, server_res, }) {
try {
// const target_path = pagePathTransform({ page_path: file_path });
// const target_root_path = root_file_path
// ? pagePathTransform({ page_path: root_file_path })
// : undefined;
const target_path = pagePathTransform({ page_path: file_path });
let tsx = ``;
const server_res_json = JSON.stringify(EJSON.stringify(server_res || {}) ?? "{}");
// Import Root from its original source path so that all sub-components
// that import __root (e.g. AppContext) resolve to the same module instance.
// Using the rewritten .bunext/pages/__root would create a separate
// createContext() call, breaking context for any sub-component that
// imports AppContext via a relative path to the source __root.
if (root_file_path) {
tsx += `import Root from "${root_file_path}"\n`;
}
tsx += `import Page from "${file_path}"\n`;
tsx += `import Page from "${target_path}"\n`;
tsx += `export default function Main() {\n\n`;
tsx += `const props = JSON.parse(${server_res_json})\n\n`;
tsx += ` return (\n`;
if (root_file_path) {
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`;

View File

@ -1,7 +0,0 @@
type Params = {
file_path: string;
};
export default function grabPageServerPath({ file_path }: Params): {
server_file_path: string | undefined;
};
export {};

View File

@ -1,11 +0,0 @@
import { existsSync } from "fs";
export default function grabPageServerPath({ file_path }) {
const page_server_ts_file = file_path.replace(/\.tsx?$/, ".server.ts");
const page_server_tsx_file = file_path.replace(/\.tsx?$/, ".server.tsx");
const server_file_path = existsSync(page_server_ts_file)
? page_server_ts_file
: existsSync(page_server_tsx_file)
? page_server_tsx_file
: undefined;
return { server_file_path };
}

View File

@ -1,9 +0,0 @@
import type { BunextPageModuleServerReturn, BunextPageServerFn, BunxRouteParams } from "../../../types";
type Params = {
url?: URL;
server_function: BunextPageServerFn;
query?: Record<string, string>;
routeParams?: BunxRouteParams;
};
export default function grabPageServerRes({ url, query, routeParams, server_function, }: Params): Promise<BunextPageModuleServerReturn>;
export {};

View File

@ -1,44 +0,0 @@
import _ from "lodash";
export default async function grabPageServerRes({ url, query, routeParams, server_function, }) {
const default_props = {
url: url
? {
..._.pick(url, [
"host",
"hostname",
"pathname",
"origin",
"port",
"search",
"searchParams",
"hash",
"href",
"password",
"protocol",
"username",
]),
}
: null,
query,
};
try {
if (routeParams) {
const serverData = await server_function({
...routeParams,
query: { ...routeParams.query, ...query },
});
return {
...serverData,
...default_props,
};
}
return {
...default_props,
};
}
catch (error) {
return {
...default_props,
};
}
}

View File

@ -1,58 +1,37 @@
import isDevelopment from "../../../utils/is-development";
import tailwindcss from "bun-plugin-tailwind";
import * as esbuild from "esbuild";
import grabDirNames from "../../../utils/grab-dir-names";
import path from "path";
import BunSkipNonBrowserPlugin from "../../bundler/plugins/bun-skip-browser-plugin";
import tailwindEsbuildPlugin from "./tailwind-esbuild-plugin";
export default async function grabTsxStringModule({ tsx, file_path, }) {
const dev = isDevelopment();
const { BUNX_CWD_MODULE_CACHE_DIR } = grabDirNames();
const trimmed_file_path = file_path
.replace(/.*\/src\/pages\//, "")
.replace(/\.tsx$/, "");
const src_file_path = path.join(BUNX_CWD_MODULE_CACHE_DIR, `${trimmed_file_path}.tsx`);
const out_file_path = path.join(BUNX_CWD_MODULE_CACHE_DIR, `${trimmed_file_path}.js`);
await Bun.write(src_file_path, tsx);
const build = await Bun.build({
entrypoints: [src_file_path],
await esbuild.build({
stdin: {
contents: tsx,
resolveDir: process.cwd(),
loader: "tsx",
},
bundle: true,
format: "esm",
target: "bun",
target: "es2020",
platform: "node",
external: ["react", "react-dom"],
minify: true,
define: {
"process.env.NODE_ENV": JSON.stringify(dev ? "development" : "production"),
},
metafile: true,
plugins: [tailwindcss, BunSkipNonBrowserPlugin],
jsx: {
runtime: "automatic",
development: dev,
},
outdir: BUNX_CWD_MODULE_CACHE_DIR,
plugins: [tailwindEsbuildPlugin],
jsx: "automatic",
write: true,
outfile: out_file_path,
});
Loader.registry.delete(out_file_path);
const module = await import(`${out_file_path}?t=${Date.now()}`);
return module;
}
// await esbuild.build({
// stdin: {
// contents: tsx,
// resolveDir: process.cwd(),
// loader: "tsx",
// },
// bundle: true,
// format: "esm",
// target: "es2020",
// platform: "node",
// external: ["react", "react-dom"],
// minify: true,
// define: {
// "process.env.NODE_ENV": JSON.stringify(
// dev ? "development" : "production",
// ),
// },
// metafile: true,
// plugins: [tailwindEsbuildPlugin],
// jsx: "automatic",
// write: true,
// outfile: out_file_path,
// });

View File

@ -2,5 +2,5 @@ import type { BunextPageModuleMeta } from "../../../types";
type Params = {
meta: BunextPageModuleMeta;
};
export default function grabWebMetaHTML({ meta }: Params): import("react/jsx-runtime").JSX.Element;
export default function grabWebMetaHTML({ meta }: Params): string;
export {};

View File

@ -1,9 +1,61 @@
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
import { escape } from "lodash";
export default function grabWebMetaHTML({ meta }) {
const keywords = meta.keywords
? Array.isArray(meta.keywords)
? meta.keywords.join(", ")
: meta.keywords
: undefined;
return (_jsxs(_Fragment, { children: [meta.title && _jsx("title", { children: meta.title }), meta.description && (_jsx("meta", { name: "description", content: meta.description })), keywords && _jsx("meta", { name: "keywords", content: keywords }), meta.author && _jsx("meta", { name: "author", content: meta.author }), meta.robots && _jsx("meta", { name: "robots", content: meta.robots }), meta.canonical && (_jsx("link", { rel: "canonical", href: meta.canonical })), meta.themeColor && (_jsx("meta", { name: "theme-color", content: meta.themeColor })), meta.og?.title && (_jsx("meta", { property: "og:title", content: meta.og.title })), meta.og?.description && (_jsx("meta", { property: "og:description", content: meta.og.description })), meta.og?.image && (_jsx("meta", { property: "og:image", content: meta.og.image })), meta.og?.url && (_jsx("meta", { property: "og:url", content: meta.og.url })), meta.og?.type && (_jsx("meta", { property: "og:type", content: meta.og.type })), meta.og?.siteName && (_jsx("meta", { property: "og:site_name", content: meta.og.siteName })), meta.og?.locale && (_jsx("meta", { property: "og:locale", content: meta.og.locale })), meta.twitter?.card && (_jsx("meta", { name: "twitter:card", content: meta.twitter.card })), meta.twitter?.title && (_jsx("meta", { name: "twitter:title", content: meta.twitter.title })), meta.twitter?.description && (_jsx("meta", { name: "twitter:description", content: meta.twitter.description })), meta.twitter?.image && (_jsx("meta", { name: "twitter:image", content: meta.twitter.image })), meta.twitter?.site && (_jsx("meta", { name: "twitter:site", content: meta.twitter.site })), meta.twitter?.creator && (_jsx("meta", { name: "twitter:creator", content: meta.twitter.creator }))] }));
let html = ``;
if (meta.title) {
html += ` <title>${escape(meta.title)}</title>\n`;
}
if (meta.description) {
html += ` <meta name="description" content="${escape(meta.description)}" />\n`;
}
if (meta.keywords) {
const keywords = Array.isArray(meta.keywords)
? meta.keywords.join(", ")
: meta.keywords;
html += ` <meta name="keywords" content="${escape(keywords)}" />\n`;
}
if (meta.author) {
html += ` <meta name="author" content="${escape(meta.author)}" />\n`;
}
if (meta.robots) {
html += ` <meta name="robots" content="${escape(meta.robots)}" />\n`;
}
if (meta.canonical) {
html += ` <link rel="canonical" href="${escape(meta.canonical)}" />\n`;
}
if (meta.themeColor) {
html += ` <meta name="theme-color" content="${escape(meta.themeColor)}" />\n`;
}
if (meta.og) {
const { og } = meta;
if (og.title)
html += ` <meta property="og:title" content="${escape(og.title)}" />\n`;
if (og.description)
html += ` <meta property="og:description" content="${escape(og.description)}" />\n`;
if (og.image)
html += ` <meta property="og:image" content="${escape(og.image)}" />\n`;
if (og.url)
html += ` <meta property="og:url" content="${escape(og.url)}" />\n`;
if (og.type)
html += ` <meta property="og:type" content="${escape(og.type)}" />\n`;
if (og.siteName)
html += ` <meta property="og:site_name" content="${escape(og.siteName)}" />\n`;
if (og.locale)
html += ` <meta property="og:locale" content="${escape(og.locale)}" />\n`;
}
if (meta.twitter) {
const { twitter } = meta;
if (twitter.card)
html += ` <meta name="twitter:card" content="${escape(twitter.card)}" />\n`;
if (twitter.title)
html += ` <meta name="twitter:title" content="${escape(twitter.title)}" />\n`;
if (twitter.description)
html += ` <meta name="twitter:description" content="${escape(twitter.description)}" />\n`;
if (twitter.image)
html += ` <meta name="twitter:image" content="${escape(twitter.image)}" />\n`;
if (twitter.site)
html += ` <meta name="twitter:site" content="${escape(twitter.site)}" />\n`;
if (twitter.creator)
html += ` <meta name="twitter:creator" content="${escape(twitter.creator)}" />\n`;
}
return html;
}

2
dist/index.d.ts vendored
View File

@ -6,7 +6,7 @@ declare const bunext: {
info: (msg: string, log?: any) => void;
success: (msg: string, log?: any) => void;
error: (msg: string | Error, log?: any) => void;
warn: (msg: string, log?: any) => void;
warn: (msg: string) => void;
build: (msg: string) => void;
watch: (msg: string) => void;
server: (url: string) => void;

View File

@ -1,4 +0,0 @@
import type { DetailedHTMLProps, HTMLAttributes, PropsWithChildren } from "react";
type Props = PropsWithChildren<DetailedHTMLProps<HTMLAttributes<HTMLHeadElement>, HTMLHeadElement>>;
export default function Head({ children, ...props }: Props): import("react/jsx-runtime").JSX.Element;
export {};

View File

@ -1,4 +0,0 @@
import { jsx as _jsx } from "react/jsx-runtime";
export default function Head({ children, ...props }) {
return _jsx("head", { ...props, children: children });
}

16
dist/types/index.d.ts vendored
View File

@ -1,5 +1,5 @@
import type { MatchedRoute, Server, WebSocketHandler } from "bun";
import type { DetailedHTMLProps, FC, HtmlHTMLAttributes, JSX, PropsWithChildren, ReactNode } from "react";
import type { FC, JSX, PropsWithChildren, ReactNode } from "react";
export type ServerProps = {
params: Record<string, string>;
searchParams: Record<string, string>;
@ -129,10 +129,11 @@ export type PageDistGenParams = {
};
export type LivePageDistGenParams = {
component: ReactNode;
head?: FC<BunextPageHeadFCProps>;
pageProps?: any;
module?: BunextPageModule;
root_module?: BunextRootModule;
bundledMap?: BundlerCTXMap;
meta?: BunextPageModuleMeta;
routeParams?: BunxRouteParams;
debug?: boolean;
};
@ -142,16 +143,11 @@ export type BunextPageHeadFCProps = {
};
export type BunextPageModule = {
default: FC<any>;
server?: BunextPageServerFn;
meta?: BunextPageModuleMeta | BunextPageModuleMetaFn;
Head?: FC<BunextPageHeadFCProps>;
config?: BunextRouteConfig;
html_props?: BunextHTMLProps;
};
export type BunextPageServerModule = {
default?: BunextPageServerFn;
server?: BunextPageServerFn;
};
export type BunextHTMLProps = DetailedHTMLProps<HtmlHTMLAttributes<HTMLHtmlElement>, HTMLHtmlElement>;
export type BunextPageModuleMetaFn = (params: {
ctx: BunxRouteParams;
serverRes?: BunextPageModuleServerReturn;
@ -239,10 +235,10 @@ export type GrabPageComponentRes = {
routeParams?: BunxRouteParams;
bundledMap?: BundlerCTXMap;
module: BunextPageModule;
root_module?: BunextRootModule;
meta?: BunextPageModuleMeta;
head?: FC<BunextPageHeadFCProps>;
debug?: boolean;
};
export type BunextRootModule = BunextPageModule;
export type GrabPageReactBundledComponentRes = {
component: JSX.Element;
server_res?: BunextPageModuleServerReturn;

View File

@ -20,9 +20,8 @@ function grabPageDirRecursively({ page_dir }) {
}
for (let i = 0; i < pages.length; i++) {
const page = pages[i];
const page_name = page.split("/").pop();
const full_page_path = path.join(page_dir, page);
if (!existsSync(full_page_path) || !page_name) {
if (!existsSync(full_page_path)) {
continue;
}
if (page.match(new RegExp(`${AppNames["RootPagesComponentName"]}`))) {
@ -31,9 +30,6 @@ function grabPageDirRecursively({ page_dir }) {
if (page.match(/\(|\)|--|\/api\//)) {
continue;
}
if (page_name.split(".").length > 2) {
continue;
}
const page_stat = statSync(full_page_path);
if (page_stat.isDirectory()) {
if (page.match(/\(|\)/))

2
dist/utils/log.d.ts vendored
View File

@ -2,7 +2,7 @@ export declare const log: {
info: (msg: string, log?: any) => void;
success: (msg: string, log?: any) => void;
error: (msg: string | Error, log?: any) => void;
warn: (msg: string, log?: any) => void;
warn: (msg: string) => void;
build: (msg: string) => void;
watch: (msg: string) => void;
server: (url: string) => void;

3
dist/utils/log.js vendored
View File

@ -3,7 +3,6 @@ import AppNames from "./grab-app-names";
const prefix = {
info: chalk.bgCyan.bold(" nfo "),
success: chalk.green.bold("✓"),
zap: chalk.green.bold("⚡"),
error: chalk.red.bold("✗"),
warn: chalk.yellow.bold("⚠"),
build: chalk.magenta.bold("⚙"),
@ -17,7 +16,7 @@ export const log = {
console.log(`${prefix.success} ${chalk.green(msg)}`, log || "");
},
error: (msg, log) => console.error(`${prefix.error} ${chalk.red(String(msg))}`, log || ""),
warn: (msg, log) => console.warn(`${prefix.warn} ${chalk.yellow(msg)}`, log || ""),
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)}`),

View File

@ -1,8 +1,6 @@
import grabAllPages from "./grab-all-pages";
import pagePathTransform from "./page-path-transform";
import stripServerSideLogic from "../functions/bundler/strip-server-side-logic";
import grabRootFilePath from "../functions/server/web-pages/grab-root-file-path";
import { existsSync } from "fs";
export default async function rewritePagesModule(params) {
const { page_file_path } = params || {};
let target_pages;
@ -17,15 +15,10 @@ export default async function rewritePagesModule(params) {
}
for (let i = 0; i < target_pages.length; i++) {
const page_path = target_pages[i];
await transformFile(page_path);
}
const { root_file_path } = grabRootFilePath();
if (root_file_path && existsSync(root_file_path)) {
await transformFile(root_file_path);
}
}
async function transformFile(page_path) {
const dst_path = pagePathTransform({ page_path });
if (page_path.match(/__root\.tsx?/)) {
continue;
}
const origin_page_content = await Bun.file(page_path).text();
const dst_page_content = stripServerSideLogic({
txt_code: origin_page_content,
@ -35,3 +28,4 @@ async function transformFile(page_path) {
createPath: true,
});
}
}

View File

@ -2,7 +2,7 @@
"name": "@moduletrace/bunext",
"module": "index.ts",
"type": "module",
"version": "1.0.3",
"version": "1.0.20",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {

View File

@ -1,5 +1,7 @@
import { describe, expect, test, beforeAll, afterAll } from "bun:test";
import startServer from "../../../src/functions/server/start-server";
import rewritePagesModule from "../../../src/utils/rewrite-pages-module";
import pagePathTransform from "../../../src/utils/page-path-transform";
import path from "path";
import fs from "fs";
@ -8,6 +10,9 @@ const fixtureDir = path.resolve(__dirname, "../../../test/e2e-fixture");
const fixturePagesDir = path.join(fixtureDir, "src", "pages");
const fixtureIndexPage = path.join(fixturePagesDir, "index.tsx");
// The rewritten page path (inside .bunext/pages, stripped of server logic)
const rewrittenIndexPage = pagePathTransform({ page_path: fixtureIndexPage });
let originalCwd = process.cwd();
let originalPort: string | undefined;
@ -32,6 +37,10 @@ describe("E2E Integration", () => {
dir: fixturePagesDir,
});
// Rewrite the fixture page (strips server logic) into .bunext/pages
// so that grab-page-react-component-string can resolve the import
await rewritePagesModule({ page_file_path: fixtureIndexPage });
// Pre-populate the bundler context map so grab-page-component can
// look up the compiled path. The `path` value only needs to be
// present for the guard check; SSR does not require the file to exist.
@ -61,6 +70,12 @@ describe("E2E Integration", () => {
delete process.env.PORT;
}
// Remove the rewritten page created during setup
const rewrittenDir = path.dirname(rewrittenIndexPage);
if (fs.existsSync(rewrittenDir)) {
fs.rmSync(rewrittenDir, { recursive: true, force: true });
}
// Remove any generated .bunext artifacts from the fixture
const dotBunext = path.join(fixtureDir, ".bunext");
if (fs.existsSync(dotBunext)) {
@ -87,41 +102,4 @@ describe("E2E Integration", () => {
// Default 404 component is rendered
expect(text).toContain("404");
});
test("server props injected from .server.ts companion file", async () => {
const serverFilePath = path.join(fixturePagesDir, "index.server.ts");
const pageFilePath = fixtureIndexPage;
// Write a temporary .server.ts companion that injects a prop
await Bun.write(serverFilePath, `
import type { BunextPageServerFn } from "../../../../../src/types";
const server: BunextPageServerFn<{ greeting: string }> = async () => {
return { props: { greeting: "Hello from server" } };
};
export default server;
`);
// Add the fixture page to the BUNDLER_CTX_MAP
global.BUNDLER_CTX_MAP[pageFilePath] = {
path: ".bunext/public/pages/index.js",
hash: "index",
type: "text/javascript",
entrypoint: pageFilePath,
local_path: pageFilePath,
url_path: "/",
file_name: "index",
};
const response = await fetch(`http://localhost:${server.port}/`);
expect(response.status).toBe(200);
const html = await response.text();
// __PAGE_PROPS__ should include the prop from the server companion
expect(html).toContain("Hello from server");
// Clean up
fs.unlinkSync(serverFilePath);
});
});

View File

@ -1,69 +0,0 @@
import { describe, it, expect, beforeAll, afterAll } from "bun:test";
import path from "path";
import fs from "fs";
import grabPageServerPath from "../../../../src/functions/server/web-pages/grab-page-server-path";
const tmpDir = path.join(import.meta.dir, "__tmp_server_path__");
beforeAll(() => {
fs.mkdirSync(tmpDir, { recursive: true });
});
afterAll(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
});
describe("grabPageServerPath", () => {
it("returns undefined when no companion file exists", () => {
const { server_file_path } = grabPageServerPath({
file_path: path.join(tmpDir, "index.tsx"),
});
expect(server_file_path).toBeUndefined();
});
it("resolves .server.ts companion for a .tsx page", () => {
const serverFile = path.join(tmpDir, "profile.server.ts");
fs.writeFileSync(serverFile, "export default async () => ({})");
const { server_file_path } = grabPageServerPath({
file_path: path.join(tmpDir, "profile.tsx"),
});
expect(server_file_path).toBe(serverFile);
});
it("resolves .server.tsx companion when only .server.tsx exists", () => {
const serverFile = path.join(tmpDir, "about.server.tsx");
fs.writeFileSync(serverFile, "export default async () => ({})");
const { server_file_path } = grabPageServerPath({
file_path: path.join(tmpDir, "about.tsx"),
});
expect(server_file_path).toBe(serverFile);
});
it("prefers .server.ts over .server.tsx when both exist", () => {
const tsFile = path.join(tmpDir, "blog.server.ts");
const tsxFile = path.join(tmpDir, "blog.server.tsx");
fs.writeFileSync(tsFile, "export default async () => ({})");
fs.writeFileSync(tsxFile, "export default async () => ({})");
const { server_file_path } = grabPageServerPath({
file_path: path.join(tmpDir, "blog.tsx"),
});
expect(server_file_path).toBe(tsFile);
});
it("resolves companion for a .ts page file", () => {
const serverFile = path.join(tmpDir, "api-page.server.ts");
fs.writeFileSync(serverFile, "export default async () => ({})");
const { server_file_path } = grabPageServerPath({
file_path: path.join(tmpDir, "api-page.ts"),
});
expect(server_file_path).toBe(serverFile);
});
});

View File

@ -1,60 +1,58 @@
import { describe, it, expect } from "bun:test";
import { renderToString } from "react-dom/server";
import grabWebMetaHTML from "../../../functions/server/web-pages/grab-web-meta-html";
function render(meta: Parameters<typeof grabWebMetaHTML>[0]["meta"]) {
return renderToString(grabWebMetaHTML({ meta }));
}
describe("grabWebMetaHTML", () => {
it("returns empty string for empty meta object", () => {
expect(render({})).toBe("");
expect(grabWebMetaHTML({ meta: {} })).toBe("");
});
it("generates a title tag", () => {
expect(render({ title: "My Page" })).toContain("<title>My Page</title>");
const html = grabWebMetaHTML({ meta: { title: "My Page" } });
expect(html).toContain("<title>My Page</title>");
});
it("generates a description meta tag", () => {
expect(render({ description: "A description" })).toContain(
'content="A description"',
);
const html = grabWebMetaHTML({ meta: { description: "A description" } });
expect(html).toContain('<meta name="description" content="A description"');
});
it("joins array keywords with comma", () => {
expect(render({ keywords: ["react", "bun", "ssr"] })).toContain(
'content="react, bun, ssr"',
);
const html = grabWebMetaHTML({
meta: { keywords: ["react", "bun", "ssr"] },
});
expect(html).toContain('content="react, bun, ssr"');
});
it("uses string keywords directly", () => {
expect(render({ keywords: "react, bun" })).toContain(
'content="react, bun"',
);
const html = grabWebMetaHTML({ meta: { keywords: "react, bun" } });
expect(html).toContain('content="react, bun"');
});
it("generates author meta tag", () => {
expect(render({ author: "Alice" })).toContain('content="Alice"');
const html = grabWebMetaHTML({ meta: { author: "Alice" } });
expect(html).toContain('<meta name="author" content="Alice"');
});
it("generates robots meta tag", () => {
expect(render({ robots: "noindex" })).toContain('content="noindex"');
const html = grabWebMetaHTML({ meta: { robots: "noindex" } });
expect(html).toContain('<meta name="robots" content="noindex"');
});
it("generates canonical link tag", () => {
expect(render({ canonical: "https://example.com/page" })).toContain(
'href="https://example.com/page"',
);
const html = grabWebMetaHTML({
meta: { canonical: "https://example.com/page" },
});
expect(html).toContain('<link rel="canonical" href="https://example.com/page"');
});
it("generates theme-color meta tag", () => {
expect(render({ themeColor: "#ff0000" })).toContain(
'content="#ff0000"',
);
const html = grabWebMetaHTML({ meta: { themeColor: "#ff0000" } });
expect(html).toContain('<meta name="theme-color" content="#ff0000"');
});
it("generates OG tags", () => {
const html = render({
const html = grabWebMetaHTML({
meta: {
og: {
title: "OG Title",
description: "OG Desc",
@ -64,19 +62,20 @@ describe("grabWebMetaHTML", () => {
siteName: "Example",
locale: "en_US",
},
},
});
expect(html).toContain('property="og:title"');
expect(html).toContain('content="OG Title"');
expect(html).toContain('property="og:description"');
expect(html).toContain('property="og:image"');
expect(html).toContain('property="og:url"');
expect(html).toContain('property="og:type"');
expect(html).toContain('property="og:site_name"');
expect(html).toContain('property="og:locale"');
expect(html).toContain('<meta property="og:title" content="OG Title"');
expect(html).toContain('<meta property="og:description" content="OG Desc"');
expect(html).toContain('<meta property="og:image" content="https://example.com/img.png"');
expect(html).toContain('<meta property="og:url" content="https://example.com"');
expect(html).toContain('<meta property="og:type" content="website"');
expect(html).toContain('<meta property="og:site_name" content="Example"');
expect(html).toContain('<meta property="og:locale" content="en_US"');
});
it("generates Twitter card tags", () => {
const html = render({
const html = grabWebMetaHTML({
meta: {
twitter: {
card: "summary_large_image",
title: "Tweet Title",
@ -85,25 +84,25 @@ describe("grabWebMetaHTML", () => {
site: "@example",
creator: "@alice",
},
},
});
expect(html).toContain('name="twitter:card"');
expect(html).toContain('content="summary_large_image"');
expect(html).toContain('name="twitter:title"');
expect(html).toContain('name="twitter:description"');
expect(html).toContain('name="twitter:image"');
expect(html).toContain('name="twitter:site"');
expect(html).toContain('name="twitter:creator"');
expect(html).toContain('<meta name="twitter:card" content="summary_large_image"');
expect(html).toContain('<meta name="twitter:title" content="Tweet Title"');
expect(html).toContain('<meta name="twitter:description" content="Tweet Desc"');
expect(html).toContain('<meta name="twitter:image" content="https://example.com/tw.png"');
expect(html).toContain('<meta name="twitter:site" content="@example"');
expect(html).toContain('<meta name="twitter:creator" content="@alice"');
});
it("skips undefined OG fields", () => {
const html = render({ og: { title: "Only Title" } });
const html = grabWebMetaHTML({ meta: { og: { title: "Only Title" } } });
expect(html).toContain("og:title");
expect(html).not.toContain("og:description");
expect(html).not.toContain("og:image");
});
it("does not emit tags for missing fields", () => {
const html = render({ title: "Hello" });
const html = grabWebMetaHTML({ meta: { title: "Hello" } });
expect(html).not.toContain("description");
expect(html).not.toContain("og:");
expect(html).not.toContain("twitter:");

View File

@ -1,11 +1,10 @@
import { Command } from "commander";
import { log } from "../../utils/log";
import init from "../../functions/init";
// import rewritePagesModule from "../../utils/rewrite-pages-module";
import rewritePagesModule from "../../utils/rewrite-pages-module";
import allPagesBunBundler from "../../functions/bundler/all-pages-bun-bundler";
import grabDirNames from "../../utils/grab-dir-names";
import { rmSync } from "fs";
import allPagesBundler from "../../functions/bundler/all-pages-bundler";
const { HYDRATION_DST_DIR, BUNX_CWD_PAGES_REWRITE_DIR } = grabDirNames();
@ -13,14 +12,15 @@ export default function () {
return new Command("build")
.description("Build Project")
.action(async () => {
process.env.NODE_ENV = "production";
process.env.BUILD = "true";
try {
rmSync(HYDRATION_DST_DIR, { recursive: true });
rmSync(BUNX_CWD_PAGES_REWRITE_DIR, { recursive: true });
} catch (error) {}
global.SKIPPED_BROWSER_MODULES = new Set<string>();
// await rewritePagesModule();
await rewritePagesModule();
await init();
log.banner();

View File

@ -2,9 +2,9 @@ import { Command } from "commander";
import startServer from "../../functions/server/start-server";
import { log } from "../../utils/log";
import bunextInit from "../../functions/bunext-init";
import rewritePagesModule from "../../utils/rewrite-pages-module";
import grabDirNames from "../../utils/grab-dir-names";
import { rmSync } from "fs";
import allPagesBunBundler from "../../functions/bundler/all-pages-bun-bundler";
const { HYDRATION_DST_DIR, BUNX_CWD_PAGES_REWRITE_DIR } = grabDirNames();
@ -21,8 +21,8 @@ export default function () {
rmSync(BUNX_CWD_PAGES_REWRITE_DIR, { recursive: true });
} catch (error) {}
await rewritePagesModule();
await bunextInit();
await allPagesBunBundler();
await startServer();
});

View File

@ -2,18 +2,17 @@ import { Command } from "commander";
import startServer from "../../functions/server/start-server";
import { log } from "../../utils/log";
import bunextInit from "../../functions/bunext-init";
import allPagesBunBundler from "../../functions/bundler/all-pages-bun-bundler";
export default function () {
return new Command("start")
.description("Start production server")
.action(async () => {
process.env.NODE_ENV = "production";
log.info("Starting production server ...");
await bunextInit();
await allPagesBunBundler();
await startServer();
});
}

View File

@ -8,7 +8,6 @@ import path from "path";
import grabClientHydrationScript from "./grab-client-hydration-script";
import { mkdirSync, rmSync } from "fs";
import recordArtifacts from "./record-artifacts";
import BunSkipNonBrowserPlugin from "./plugins/bun-skip-browser-plugin";
const { HYDRATION_DST_DIR, BUNX_HYDRATION_SRC_DIR, BUNX_TMP_DIR } =
grabDirNames();
@ -56,19 +55,17 @@ export default async function allPagesBunBundler(params?: Params) {
const buildStart = performance.now();
const define = {
"process.env.NODE_ENV": JSON.stringify(
dev ? "development" : "production",
),
};
const result = await Bun.build({
entrypoints: [...entryToPage.keys()],
outdir: HYDRATION_DST_DIR,
root: BUNX_HYDRATION_SRC_DIR,
minify: !dev,
minify: true,
format: "esm",
define,
define: {
"process.env.NODE_ENV": JSON.stringify(
dev ? "development" : "production",
),
},
naming: {
entry: "[dir]/[hash].[ext]",
chunk: "chunks/[hash].[ext]",
@ -127,10 +124,7 @@ export default async function allPagesBunBundler(params?: Params) {
}
if (artifacts?.[0]) {
await recordArtifacts({
artifacts,
page_file_paths,
});
await recordArtifacts({ artifacts });
}
const elapsed = (performance.now() - buildStart).toFixed(0);

View File

@ -9,7 +9,6 @@ import grabArtifactsFromBundledResults from "./grab-artifacts-from-bundled-resul
import { writeFileSync } from "fs";
import type { BundlerCTXMap } from "../../types";
import recordArtifacts from "./record-artifacts";
import stripServerSideLogic from "./strip-server-side-logic";
const { HYDRATION_DST_DIR, HYDRATION_DST_DIR_MAP_JSON_FILE } = grabDirNames();
@ -40,7 +39,7 @@ export default async function allPagesBundler(params?: Params) {
const dev = isDevelopment();
for (const page of target_pages) {
const key = page.local_path;
const key = page.transformed_path;
const txt = await grabClientHydrationScript({
page_local_path: page.local_path,
@ -52,13 +51,6 @@ export default async function allPagesBundler(params?: Params) {
if (!txt) continue;
// const final_tsx = stripServerSideLogic({
// txt_code: txt,
// file_path: key,
// });
// console.log("final_tsx", final_tsx);
virtualEntries[key] = txt;
}
@ -102,31 +94,6 @@ export default async function allPagesBundler(params?: Params) {
const entryPoints = Object.keys(virtualEntries).map((k) => `virtual:${k}`);
// let alias: any = {};
// const excludes = [
// "bun:sqlite",
// "path",
// "url",
// "events",
// "util",
// "crypto",
// "net",
// "tls",
// "fs",
// "node:path",
// "node:url",
// "node:process",
// "node:fs",
// "node:timers/promises",
// ];
// for (let i = 0; i < excludes.length; i++) {
// const exclude = excludes[i];
// alias[exclude] = "./empty.js";
// }
// console.log("alias", alias);
const result = await esbuild.build({
entryPoints,
outdir: HYDRATION_DST_DIR,
@ -152,7 +119,6 @@ export default async function allPagesBundler(params?: Params) {
"react-dom/client",
"react/jsx-runtime",
],
// alias,
});
if (result.errors.length > 0) {

View File

@ -4,7 +4,6 @@ import grabDirNames from "../../utils/grab-dir-names";
import AppNames from "../../utils/grab-app-names";
import grabConstants from "../../utils/grab-constants";
import pagePathTransform from "../../utils/page-path-transform";
import grabRootFilePath from "../server/web-pages/grab-root-file-path";
const { PAGES_DIR } = grabDirNames();
@ -21,26 +20,28 @@ export default async function grabClientHydrationScript({
ClientWindowPagePropsName,
} = grabConstants();
const { root_file_path } = grabRootFilePath();
const target_path = pagePathTransform({ page_path: page_local_path });
// const target_path = pagePathTransform({ page_path: page_local_path });
// const target_root_path = root_file_path
// ? pagePathTransform({ page_path: root_file_path })
// : undefined;
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 (root_file_path) {
txt += `import Root from "${root_file_path}";\n`;
if (does_root_exist) {
txt += `import Root from "${root_component_path}";\n`;
}
txt += `import Page from "${page_local_path}";\n\n`;
txt += `import Page from "${target_path}";\n\n`;
txt += `const pageProps = window.${ClientWindowPagePropsName} || {};\n`;
if (root_file_path) {
txt += `const component = <Root {...pageProps}><Page {...pageProps} /></Root>\n`;
if (does_root_exist) {
txt += `const component = <Root suppressHydrationWarning={true} {...pageProps}><Page {...pageProps} /></Root>\n`;
} else {
txt += `const component = <Page {...pageProps} />\n`;
txt += `const component = <Page suppressHydrationWarning={true} {...pageProps} />\n`;
}
txt += `if (window.${ClientRootComponentWindowName}?.render) {\n`;

View File

@ -1,84 +1,33 @@
import { log } from "../../../utils/log";
const BunSkipNonBrowserPlugin: Bun.BunPlugin = {
name: "skip-non-browser",
setup(build) {
const skipFilter =
/^(bun:|node:|fs$|path$|os$|crypto$|net$|events$|util$|tls$|url$|process$)/;
// const skipped_modules = new Set<string>();
build.onResolve({ filter: skipFilter }, (args) => {
global.SKIPPED_BROWSER_MODULES.add(args.path);
return {
path: args.path,
namespace: "skipped",
// external: true,
};
build.onResolve({ filter: /^(bun:|node:)/ }, (args) => {
return { path: args.path, external: true };
});
// build.onEnd(() => {
// log.warn(`global.SKIPPED_BROWSER_MODULES`, [
// ...global.SKIPPED_BROWSER_MODULES,
// ]);
// });
build.onResolve({ filter: /^[^./]/ }, (args) => {
// If it's a built-in like 'fs' or 'path', skip it immediately
const excludes = [
"fs",
"path",
"os",
"crypto",
"net",
"events",
"util",
];
// build.onResolve({ filter: /^[^./]/ }, (args) => {
// // If it's a built-in like 'fs' or 'path', skip it immediately
// const excludes = [
// "fs",
// "path",
// "os",
// "crypto",
// "net",
// "events",
// "util",
// "tls",
// ];
if (excludes.includes(args.path) || args.path.startsWith("node:")) {
return { path: args.path, external: true };
}
// if (excludes.includes(args.path) || args.path.startsWith("node:")) {
// return {
// path: args.path,
// // namespace: "skipped",
// external: true,
// };
// }
// try {
// Bun.resolveSync(args.path, args.importer || process.cwd());
// return null;
// } catch (e) {
// console.warn(`[Skip] Mark as external: ${args.path}`);
// return {
// path: args.path,
// // namespace: "skipped",
// external: true,
// };
// }
// });
build.onLoad({ filter: /.*/, namespace: "skipped" }, (args) => {
return {
contents: `
const proxy = new Proxy(() => proxy, {
get: () => proxy,
construct: () => proxy,
});
export const Database = proxy;
export const join = proxy;
export const fileURLToPath = proxy;
export const arch = proxy;
export const platform = proxy;
export const statSync = proxy;
export const $H = proxy;
export const _ = proxy;
export default proxy;
`,
loader: "js",
};
try {
Bun.resolveSync(args.path, args.importer || process.cwd());
return null;
} catch (e) {
console.warn(`[Skip] Mark as external: ${args.path}`);
return { path: args.path, external: true };
}
});
},
};

View File

@ -1,18 +1,13 @@
import grabDirNames from "../../utils/grab-dir-names";
import type { BundlerCTXMap } from "../../types";
import _ from "lodash";
const { HYDRATION_DST_DIR_MAP_JSON_FILE } = grabDirNames();
type Params = {
artifacts: BundlerCTXMap[];
page_file_paths?: string[];
};
export default async function recordArtifacts({
artifacts,
page_file_paths,
}: Params) {
export default async function recordArtifacts({ artifacts }: Params) {
const artifacts_map: { [k: string]: BundlerCTXMap } = {};
for (const artifact of artifacts) {
@ -22,11 +17,11 @@ export default async function recordArtifacts({
}
if (global.BUNDLER_CTX_MAP) {
global.BUNDLER_CTX_MAP = _.merge(global.BUNDLER_CTX_MAP, artifacts_map);
global.BUNDLER_CTX_MAP = artifacts_map;
}
// await Bun.write(
// HYDRATION_DST_DIR_MAP_JSON_FILE,
// JSON.stringify(artifacts_map, null, 4),
// );
await Bun.write(
HYDRATION_DST_DIR_MAP_JSON_FILE,
JSON.stringify(artifacts_map, null, 4),
);
}

View File

@ -7,12 +7,14 @@ import type {
} from "../types";
import type { FileSystemRouter, Server } from "bun";
import grabDirNames from "../utils/grab-dir-names";
import { type FSWatcher } from "fs";
import { readFileSync, type FSWatcher } from "fs";
import init from "./init";
import isDevelopment from "../utils/is-development";
import allPagesBundler from "./bundler/all-pages-bundler";
import watcher from "./server/watcher";
import { log } from "../utils/log";
import cron from "./server/cron";
import EJSON from "../utils/ejson";
import allPagesBunBundler from "./bundler/all-pages-bun-bundler";
/**
@ -33,7 +35,7 @@ declare global {
var CURRENT_VERSION: string | undefined;
var PAGE_FILES: PageFiles[];
var ROOT_FILE_UPDATED: boolean;
var SKIPPED_BROWSER_MODULES: Set<string>;
// var BUNDLER_CTX: BuildContext | undefined;
}
const { PAGES_DIR, HYDRATION_DST_DIR_MAP_JSON_FILE } = grabDirNames();
@ -45,7 +47,6 @@ export default async function bunextInit() {
global.BUNDLER_CTX_MAP = {};
global.BUNDLER_REBUILDS = 0;
global.PAGE_FILES = [];
global.SKIPPED_BROWSER_MODULES = new Set<string>();
await init();
log.banner();
@ -60,8 +61,18 @@ export default async function bunextInit() {
const is_dev = isDevelopment();
if (is_dev) {
// await allPagesBundler();
await allPagesBunBundler();
watcher();
} else {
const artifacts = EJSON.parse(
readFileSync(HYDRATION_DST_DIR_MAP_JSON_FILE, "utf-8"),
) as { [k: string]: BundlerCTXMap } | undefined;
if (!artifacts) {
log.error("Please build first.");
process.exit(1);
}
global.BUNDLER_CTX_MAP = artifacts;
cron();
}
}

View File

@ -3,7 +3,7 @@ import path from "path";
import grabDirNames from "../../utils/grab-dir-names";
import rebuildBundler from "./rebuild-bundler";
import { log } from "../../utils/log";
// import rewritePagesModule from "../../utils/rewrite-pages-module";
import rewritePagesModule from "../../utils/rewrite-pages-module";
const { ROOT_DIR } = grabDirNames();
@ -82,18 +82,19 @@ async function fullRebuild(params?: { msg?: string }) {
global.RECOMPILING = true;
// const target_file_paths = global.HMR_CONTROLLERS.map(
// (hmr) => hmr.target_map?.local_path,
// ).filter((f) => typeof f == "string");
const target_file_paths = global.HMR_CONTROLLERS.map(
(hmr) => hmr.target_map?.local_path,
).filter((f) => typeof f == "string");
// await rewritePagesModule();
await rewritePagesModule({
page_file_path: target_file_paths,
});
if (msg) {
log.watch(msg);
}
await rebuildBundler();
// await rebuildBundler({ target_file_paths });
await rebuildBundler({ target_file_paths });
} catch (error: any) {
log.error(error);
} finally {

View File

@ -24,159 +24,78 @@ export default async function genWebHTML({
component,
pageProps,
bundledMap,
head: Head,
module,
meta,
routeParams,
debug,
root_module,
}: LivePageDistGenParams) {
const { ClientRootElementIDName, ClientWindowPagePropsName } =
grabContants();
const is_dev = isDevelopment();
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} />)
: "";
let html = `<!DOCTYPE html>\n`;
html += `<html>\n`;
html += ` <head>\n`;
html += ` <meta charset="utf-8" />\n`;
html += ` <meta name="viewport" content="width=device-width, initial-scale=1.0">\n`;
if (meta) {
html += ` ${grabWebMetaHTML({ meta })}\n`;
}
if (bundledMap?.css_path) {
html += ` <link rel="stylesheet" href="/${bundledMap.css_path}" />\n`;
}
const serializedProps = (EJSON.stringify(pageProps || {}) || "{}").replace(
/<\//g,
"<\\/",
);
html += ` <script>window.${ClientWindowPagePropsName} = ${serializedProps}</script>\n`;
const page_hydration_script = await grabWebPageHydrationScript();
const root_meta = root_module?.meta
? typeof root_module.meta == "function" && routeParams
? await root_module.meta({ ctx: routeParams, serverRes: pageProps })
: typeof root_module.meta == "function"
? undefined
: root_module.meta
: undefined;
const page_meta = module?.meta
? typeof module.meta == "function" && routeParams
? await module.meta({ ctx: routeParams, serverRes: pageProps })
: typeof module.meta == "function"
? undefined
: module.meta
: undefined;
const html_props = {
...module?.html_props,
...root_module?.html_props,
};
const Head = module?.Head;
const RootHead = root_module?.Head;
if (bundledMap?.path) {
const dev = isDevelopment();
const devSuffix = dev ? "?dev" : "";
const browser_imports: Record<string, string> = {
react: `https://esm.sh/react@${_reactVersion}`,
"react-dom": `https://esm.sh/react-dom@${_reactVersion}`,
"react-dom/client": `https://esm.sh/react-dom@${_reactVersion}/client`,
"react/jsx-runtime": `https://esm.sh/react@${_reactVersion}/jsx-runtime`,
};
if (dev) {
browser_imports["react/jsx-dev-runtime"] =
`https://esm.sh/react@${_reactVersion}/jsx-dev-runtime`;
const importMap = JSON.stringify({
imports: {
react: `https://esm.sh/react@${_reactVersion}${devSuffix}`,
"react-dom": `https://esm.sh/react-dom@${_reactVersion}${devSuffix}`,
"react-dom/client": `https://esm.sh/react-dom@${_reactVersion}/client${devSuffix}`,
"react/jsx-runtime": `https://esm.sh/react@${_reactVersion}/jsx-runtime${devSuffix}`,
"react/jsx-dev-runtime": `https://esm.sh/react@${_reactVersion}/jsx-dev-runtime${devSuffix}`,
},
});
html += ` <script type="importmap">${importMap}</script>\n`;
html += ` <script src="/${bundledMap.path}" type="module" id="${AppData["BunextClientHydrationScriptID"]}" async></script>\n`;
}
const importMap = JSON.stringify({
imports: browser_imports,
});
if (isDevelopment()) {
html += `<script defer>\n${await grabWebPageHydrationScript()}\n</script>\n`;
}
let final_component = (
<html {...html_props}>
<head>
<meta charSet="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
if (headHTML) {
html += ` ${headHTML}\n`;
}
{root_meta ? grabWebMetaHTML({ meta: root_meta }) : null}
{page_meta ? grabWebMetaHTML({ meta: page_meta }) : null}
{bundledMap?.css_path ? (
<link rel="stylesheet" href={`/${bundledMap.css_path}`} />
) : null}
<script
dangerouslySetInnerHTML={{
__html: `window.${ClientWindowPagePropsName} = ${serializedProps}`,
}}
/>
{/* {global.SKIPPED_BROWSER_MODULES ? (
<script
type="importmap"
dangerouslySetInnerHTML={{
__html: importMap,
}}
fetchPriority="high"
/>
) : null} */}
{bundledMap?.path ? (
<>
<script
type="importmap"
dangerouslySetInnerHTML={{
__html: importMap,
}}
fetchPriority="high"
/>
<script
src={`/${bundledMap.path}`}
type="module"
id={AppData["BunextClientHydrationScriptID"]}
defer
/>
</>
) : null}
{is_dev ? (
<script
defer
dangerouslySetInnerHTML={{
__html: page_hydration_script,
}}
/>
) : null}
{RootHead ? (
<RootHead serverRes={pageProps} ctx={routeParams} />
) : null}
{Head ? <Head serverRes={pageProps} ctx={routeParams} /> : null}
</head>
<body>
<div
id={ClientRootElementIDName}
suppressHydrationWarning={!dev}
>
{component}
</div>
</body>
</html>
);
let html = `<!DOCTYPE html>\n`;
// const stream = await renderToReadableStream(final_component, {
// onError(error: any) {
// // This is where you "omit" or handle the errors
// // You can log it silently or ignore it
// if (error.message.includes('unique "key" prop')) return;
// console.error(error);
// },
// });
// // 2. Convert the Web Stream to a String (Bun-optimized)
// const htmlBody = await new Response(stream).text();
// html += htmlBody;
html += renderToString(final_component);
html += ` </head>\n`;
html += ` <body>\n`;
html += ` <div id="${ClientRootElementIDName}">${componentHTML}</div>\n`;
html += ` </body>\n`;
html += `</html>\n`;
return html;
}

View File

@ -1,4 +1,3 @@
import _ from "lodash";
import type { GrabPageComponentRes } from "../../../types";
import isDevelopment from "../../../utils/is-development";
import { log } from "../../../utils/log";
@ -9,19 +8,21 @@ export default async function generateWebPageResponseFromComponentReturn({
component,
module,
bundledMap,
head,
meta,
routeParams,
serverRes,
debug,
root_module,
}: GrabPageComponentRes) {
const html = await genWebHTML({
component,
pageProps: serverRes,
bundledMap,
module,
meta,
head,
routeParams,
debug,
root_module,
});
if (debug) {
@ -54,10 +55,9 @@ export default async function generateWebPageResponseFromComponentReturn({
};
}
const config = _.merge(root_module?.config, module.config);
const cache_page = config?.cachePage || serverRes?.cachePage || false;
const expiry_seconds = config?.cacheExpiry || serverRes?.cacheExpiry;
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 || "");

View File

@ -1,9 +1,8 @@
import type { FC } from "react";
import grabRouteParams from "../../../utils/grab-route-params";
import type {
BunextPageModule,
BunextPageModuleServerReturn,
BunextPageServerModule,
BunextRootModule,
BunxRouteParams,
GrabPageComponentRes,
} from "../../../types";
@ -12,8 +11,6 @@ import grabPageBundledReactComponent from "./grab-page-bundled-react-component";
import _ from "lodash";
import { log } from "../../../utils/log";
import grabRootFilePath from "./grab-root-file-path";
import grabPageServerRes from "./grab-page-server-res";
import grabPageServerPath from "./grab-page-server-path";
class NotFoundError extends Error {}
@ -68,7 +65,6 @@ export default async function grabPageComponent({
const bundledMap = global.BUNDLER_CTX_MAP?.[file_path];
if (!bundledMap?.path) {
console.log(global.BUNDLER_CTX_MAP);
const errMsg = `No Bundled File Path for this request path!`;
log.error(errMsg);
throw new Error(errMsg);
@ -79,65 +75,81 @@ export default async function grabPageComponent({
}
const { root_file_path } = grabRootFilePath();
const root_module: BunextRootModule | undefined = root_file_path
? await import(`${root_file_path}?t=${now}`)
: undefined;
const { server_file_path: root_server_file_path } = root_file_path
? grabPageServerPath({ file_path: root_file_path })
: {};
const root_server_module: BunextPageServerModule = root_server_file_path
? await import(`${root_server_file_path}?t=${now}`)
: undefined;
const root_server_fn =
root_server_module?.default || root_server_module?.server;
const rootServerRes: BunextPageModuleServerReturn | undefined =
root_server_fn
? await grabPageServerRes({
server_function: root_server_fn,
url,
query: match?.query,
routeParams,
})
: undefined;
if (debug) {
log.info(`rootServerRes:`, rootServerRes);
}
const module: BunextPageModule = await import(`${file_path}?t=${now}`);
const { server_file_path } = grabPageServerPath({ file_path });
const server_module: BunextPageServerModule = server_file_path
? await import(`${server_file_path}?t=${now}`)
: undefined;
if (debug) {
log.info(`module:`, module);
}
const server_fn = server_module?.default || server_module?.server;
const serverRes: BunextPageModuleServerReturn | undefined = server_fn
? await grabPageServerRes({
server_function: server_fn,
url,
const serverRes: BunextPageModuleServerReturn = await (async () => {
const default_props: BunextPageModuleServerReturn = {
url: {
...(_.pick<URL, keyof URL>(url!, [
"host",
"hostname",
"pathname",
"origin",
"port",
"search",
"searchParams",
"hash",
"href",
"password",
"protocol",
"username",
]) as any),
},
query: match?.query,
routeParams,
})
: undefined;
};
try {
if (routeParams) {
const serverData = await module["server"]?.({
...routeParams,
query: { ...routeParams.query, ...match?.query },
});
return {
...serverData,
...default_props,
};
}
return {
...default_props,
};
} catch (error) {
return {
...default_props,
};
}
})();
if (debug) {
log.info(`serverRes:`, serverRes);
}
const mergedServerRes = _.merge(rootServerRes || {}, serverRes || {});
const meta = module.meta
? typeof module.meta == "function" && routeParams
? await module.meta({
ctx: routeParams,
serverRes,
})
: typeof module.meta == "object"
? module.meta
: undefined
: undefined;
if (debug) {
log.info(`meta:`, meta);
}
const Head = module.Head as FC<any>;
const { component } =
(await grabPageBundledReactComponent({
file_path,
root_file_path,
server_res: mergedServerRes,
server_res: serverRes,
})) || {};
if (!component) {
@ -150,11 +162,12 @@ export default async function grabPageComponent({
return {
component,
serverRes: mergedServerRes,
serverRes,
routeParams,
module,
bundledMap,
root_module,
meta,
head: Head,
};
} catch (error: any) {
log.error(`Error Grabbing Page Component: ${error.message}`);

View File

@ -13,34 +13,25 @@ export default function grabPageReactComponentString({
server_res,
}: Params): string | undefined {
try {
// const target_path = pagePathTransform({ page_path: file_path });
// const target_root_path = root_file_path
// ? pagePathTransform({ page_path: root_file_path })
// : undefined;
const target_path = pagePathTransform({ page_path: file_path });
let tsx = ``;
const server_res_json = JSON.stringify(
EJSON.stringify(server_res || {}) ?? "{}",
);
// Import Root from its original source path so that all sub-components
// that import __root (e.g. AppContext) resolve to the same module instance.
// Using the rewritten .bunext/pages/__root would create a separate
// createContext() call, breaking context for any sub-component that
// imports AppContext via a relative path to the source __root.
if (root_file_path) {
tsx += `import Root from "${root_file_path}"\n`;
}
tsx += `import Page from "${file_path}"\n`;
tsx += `import Page from "${target_path}"\n`;
tsx += `export default function Main() {\n\n`;
tsx += `const props = JSON.parse(${server_res_json})\n\n`;
tsx += ` return (\n`;
if (root_file_path) {
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`;

View File

@ -1,18 +0,0 @@
import { existsSync } from "fs";
type Params = {
file_path: string;
};
export default function grabPageServerPath({ file_path }: Params) {
const page_server_ts_file = file_path.replace(/\.tsx?$/, ".server.ts");
const page_server_tsx_file = file_path.replace(/\.tsx?$/, ".server.tsx");
const server_file_path = existsSync(page_server_ts_file)
? page_server_ts_file
: existsSync(page_server_tsx_file)
? page_server_tsx_file
: undefined;
return { server_file_path };
}

View File

@ -1,64 +0,0 @@
import type {
BunextPageModule,
BunextPageModuleServerReturn,
BunextPageServerFn,
BunxRouteParams,
GrabPageComponentRes,
} from "../../../types";
import _ from "lodash";
type Params = {
url?: URL;
server_function: BunextPageServerFn;
query?: Record<string, string>;
routeParams?: BunxRouteParams;
};
export default async function grabPageServerRes({
url,
query,
routeParams,
server_function,
}: Params): Promise<BunextPageModuleServerReturn> {
const default_props: BunextPageModuleServerReturn = {
url: url
? {
...(_.pick<URL, keyof URL>(url, [
"host",
"hostname",
"pathname",
"origin",
"port",
"search",
"searchParams",
"hash",
"href",
"password",
"protocol",
"username",
]) as any),
}
: null,
query,
};
try {
if (routeParams) {
const serverData = await server_function({
...routeParams,
query: { ...routeParams.query, ...query },
});
return {
...serverData,
...default_props,
};
}
return {
...default_props,
};
} catch (error) {
return {
...default_props,
};
}
}

View File

@ -1,8 +1,8 @@
import isDevelopment from "../../../utils/is-development";
import tailwindcss from "bun-plugin-tailwind";
import * as esbuild from "esbuild";
import grabDirNames from "../../../utils/grab-dir-names";
import path from "path";
import BunSkipNonBrowserPlugin from "../../bundler/plugins/bun-skip-browser-plugin";
import tailwindEsbuildPlugin from "./tailwind-esbuild-plugin";
type Params = {
tsx: string;
@ -20,22 +20,21 @@ export default async function grabTsxStringModule<T extends any = any>({
.replace(/.*\/src\/pages\//, "")
.replace(/\.tsx$/, "");
const src_file_path = path.join(
BUNX_CWD_MODULE_CACHE_DIR,
`${trimmed_file_path}.tsx`,
);
const out_file_path = path.join(
BUNX_CWD_MODULE_CACHE_DIR,
`${trimmed_file_path}.js`,
);
await Bun.write(src_file_path, tsx);
const build = await Bun.build({
entrypoints: [src_file_path],
await esbuild.build({
stdin: {
contents: tsx,
resolveDir: process.cwd(),
loader: "tsx",
},
bundle: true,
format: "esm",
target: "bun",
target: "es2020",
platform: "node",
external: ["react", "react-dom"],
minify: true,
define: {
@ -44,12 +43,10 @@ export default async function grabTsxStringModule<T extends any = any>({
),
},
metafile: true,
plugins: [tailwindcss, BunSkipNonBrowserPlugin],
jsx: {
runtime: "automatic",
development: dev,
},
outdir: BUNX_CWD_MODULE_CACHE_DIR,
plugins: [tailwindEsbuildPlugin],
jsx: "automatic",
write: true,
outfile: out_file_path,
});
Loader.registry.delete(out_file_path);
@ -57,27 +54,3 @@ export default async function grabTsxStringModule<T extends any = any>({
return module as T;
}
// await esbuild.build({
// stdin: {
// contents: tsx,
// resolveDir: process.cwd(),
// loader: "tsx",
// },
// bundle: true,
// format: "esm",
// target: "es2020",
// platform: "node",
// external: ["react", "react-dom"],
// minify: true,
// define: {
// "process.env.NODE_ENV": JSON.stringify(
// dev ? "development" : "production",
// ),
// },
// metafile: true,
// plugins: [tailwindEsbuildPlugin],
// jsx: "automatic",
// write: true,
// outfile: out_file_path,
// });

View File

@ -0,0 +1,77 @@
import { escape } from "lodash";
import type { BunextPageModuleMeta } from "../../../types";
type Params = {
meta: BunextPageModuleMeta;
};
export default function grabWebMetaHTML({ meta }: Params) {
let html = ``;
if (meta.title) {
html += ` <title>${escape(meta.title)}</title>\n`;
}
if (meta.description) {
html += ` <meta name="description" content="${escape(meta.description)}" />\n`;
}
if (meta.keywords) {
const keywords = Array.isArray(meta.keywords)
? meta.keywords.join(", ")
: meta.keywords;
html += ` <meta name="keywords" content="${escape(keywords)}" />\n`;
}
if (meta.author) {
html += ` <meta name="author" content="${escape(meta.author)}" />\n`;
}
if (meta.robots) {
html += ` <meta name="robots" content="${escape(meta.robots)}" />\n`;
}
if (meta.canonical) {
html += ` <link rel="canonical" href="${escape(meta.canonical)}" />\n`;
}
if (meta.themeColor) {
html += ` <meta name="theme-color" content="${escape(meta.themeColor)}" />\n`;
}
if (meta.og) {
const { og } = meta;
if (og.title)
html += ` <meta property="og:title" content="${escape(og.title)}" />\n`;
if (og.description)
html += ` <meta property="og:description" content="${escape(og.description)}" />\n`;
if (og.image)
html += ` <meta property="og:image" content="${escape(og.image)}" />\n`;
if (og.url)
html += ` <meta property="og:url" content="${escape(og.url)}" />\n`;
if (og.type)
html += ` <meta property="og:type" content="${escape(og.type)}" />\n`;
if (og.siteName)
html += ` <meta property="og:site_name" content="${escape(og.siteName)}" />\n`;
if (og.locale)
html += ` <meta property="og:locale" content="${escape(og.locale)}" />\n`;
}
if (meta.twitter) {
const { twitter } = meta;
if (twitter.card)
html += ` <meta name="twitter:card" content="${escape(twitter.card)}" />\n`;
if (twitter.title)
html += ` <meta name="twitter:title" content="${escape(twitter.title)}" />\n`;
if (twitter.description)
html += ` <meta name="twitter:description" content="${escape(twitter.description)}" />\n`;
if (twitter.image)
html += ` <meta name="twitter:image" content="${escape(twitter.image)}" />\n`;
if (twitter.site)
html += ` <meta name="twitter:site" content="${escape(twitter.site)}" />\n`;
if (twitter.creator)
html += ` <meta name="twitter:creator" content="${escape(twitter.creator)}" />\n`;
}
return html;
}

View File

@ -1,76 +0,0 @@
import type { BunextPageModuleMeta } from "../../../types";
type Params = {
meta: BunextPageModuleMeta;
};
export default function grabWebMetaHTML({ meta }: Params) {
const keywords = meta.keywords
? Array.isArray(meta.keywords)
? meta.keywords.join(", ")
: meta.keywords
: undefined;
return (
<>
{meta.title && <title>{meta.title}</title>}
{meta.description && (
<meta name="description" content={meta.description} />
)}
{keywords && <meta name="keywords" content={keywords} />}
{meta.author && <meta name="author" content={meta.author} />}
{meta.robots && <meta name="robots" content={meta.robots} />}
{meta.canonical && (
<link rel="canonical" href={meta.canonical} />
)}
{meta.themeColor && (
<meta name="theme-color" content={meta.themeColor} />
)}
{meta.og?.title && (
<meta property="og:title" content={meta.og.title} />
)}
{meta.og?.description && (
<meta
property="og:description"
content={meta.og.description}
/>
)}
{meta.og?.image && (
<meta property="og:image" content={meta.og.image} />
)}
{meta.og?.url && (
<meta property="og:url" content={meta.og.url} />
)}
{meta.og?.type && (
<meta property="og:type" content={meta.og.type} />
)}
{meta.og?.siteName && (
<meta property="og:site_name" content={meta.og.siteName} />
)}
{meta.og?.locale && (
<meta property="og:locale" content={meta.og.locale} />
)}
{meta.twitter?.card && (
<meta name="twitter:card" content={meta.twitter.card} />
)}
{meta.twitter?.title && (
<meta name="twitter:title" content={meta.twitter.title} />
)}
{meta.twitter?.description && (
<meta
name="twitter:description"
content={meta.twitter.description}
/>
)}
{meta.twitter?.image && (
<meta name="twitter:image" content={meta.twitter.image} />
)}
{meta.twitter?.site && (
<meta name="twitter:site" content={meta.twitter.site} />
)}
{meta.twitter?.creator && (
<meta name="twitter:creator" content={meta.twitter.creator} />
)}
</>
);
}

View File

@ -1,13 +0,0 @@
import type {
DetailedHTMLProps,
HTMLAttributes,
PropsWithChildren,
} from "react";
type Props = PropsWithChildren<
DetailedHTMLProps<HTMLAttributes<HTMLHeadElement>, HTMLHeadElement>
>;
export default function Head({ children, ...props }: Props) {
return <head {...props}>{children}</head>;
}

View File

@ -1,12 +1,5 @@
import type { MatchedRoute, Server, WebSocketHandler } from "bun";
import type {
DetailedHTMLProps,
FC,
HtmlHTMLAttributes,
JSX,
PropsWithChildren,
ReactNode,
} from "react";
import type { FC, JSX, PropsWithChildren, ReactNode } from "react";
export type ServerProps = {
params: Record<string, string>;
@ -152,10 +145,11 @@ export type PageDistGenParams = {
export type LivePageDistGenParams = {
component: ReactNode;
head?: FC<BunextPageHeadFCProps>;
pageProps?: any;
module?: BunextPageModule;
root_module?: BunextRootModule;
bundledMap?: BundlerCTXMap;
meta?: BunextPageModuleMeta;
routeParams?: BunxRouteParams;
debug?: boolean;
};
@ -167,22 +161,12 @@ export type BunextPageHeadFCProps = {
export type BunextPageModule = {
default: FC<any>;
server?: BunextPageServerFn;
meta?: BunextPageModuleMeta | BunextPageModuleMetaFn;
Head?: FC<BunextPageHeadFCProps>;
config?: BunextRouteConfig;
html_props?: BunextHTMLProps;
};
export type BunextPageServerModule = {
default?: BunextPageServerFn;
server?: BunextPageServerFn;
};
export type BunextHTMLProps = DetailedHTMLProps<
HtmlHTMLAttributes<HTMLHtmlElement>,
HTMLHtmlElement
>;
export type BunextPageModuleMetaFn = (params: {
ctx: BunxRouteParams;
serverRes?: BunextPageModuleServerReturn;
@ -269,12 +253,11 @@ export type GrabPageComponentRes = {
routeParams?: BunxRouteParams;
bundledMap?: BundlerCTXMap;
module: BunextPageModule;
root_module?: BunextRootModule;
meta?: BunextPageModuleMeta;
head?: FC<BunextPageHeadFCProps>;
debug?: boolean;
};
export type BunextRootModule = BunextPageModule;
export type GrabPageReactBundledComponentRes = {
component: JSX.Element;
server_res?: BunextPageModuleServerReturn;

View File

@ -33,10 +33,9 @@ function grabPageDirRecursively({ page_dir }: { page_dir: string }) {
for (let i = 0; i < pages.length; i++) {
const page = pages[i];
const page_name = page.split("/").pop();
const full_page_path = path.join(page_dir, page);
if (!existsSync(full_page_path) || !page_name) {
if (!existsSync(full_page_path)) {
continue;
}
@ -48,10 +47,6 @@ function grabPageDirRecursively({ page_dir }: { page_dir: string }) {
continue;
}
if (page_name.split(".").length > 2) {
continue;
}
const page_stat = statSync(full_page_path);
if (page_stat.isDirectory()) {

View File

@ -4,7 +4,6 @@ import AppNames from "./grab-app-names";
const prefix = {
info: chalk.bgCyan.bold(" nfo "),
success: chalk.green.bold("✓"),
zap: chalk.green.bold("⚡"),
error: chalk.red.bold("✗"),
warn: chalk.yellow.bold("⚠"),
build: chalk.magenta.bold("⚙"),
@ -20,8 +19,7 @@ export const log = {
},
error: (msg: string | Error, log?: any) =>
console.error(`${prefix.error} ${chalk.red(String(msg))}`, log || ""),
warn: (msg: string, log?: any) =>
console.warn(`${prefix.warn} ${chalk.yellow(msg)}`, log || ""),
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)}`),

View File

@ -1,8 +1,6 @@
import grabAllPages from "./grab-all-pages";
import pagePathTransform from "./page-path-transform";
import stripServerSideLogic from "../functions/bundler/strip-server-side-logic";
import grabRootFilePath from "../functions/server/web-pages/grab-root-file-path";
import { existsSync } from "fs";
type Params = {
page_file_path?: string | string[];
@ -23,19 +21,12 @@ export default async function rewritePagesModule(params?: Params) {
for (let i = 0; i < target_pages.length; i++) {
const page_path = target_pages[i];
await transformFile(page_path);
}
const { root_file_path } = grabRootFilePath();
if (root_file_path && existsSync(root_file_path)) {
await transformFile(root_file_path);
}
}
async function transformFile(page_path: string) {
const dst_path = pagePathTransform({ page_path });
if (page_path.match(/__root\.tsx?/)) {
continue;
}
const origin_page_content = await Bun.file(page_path).text();
const dst_page_content = stripServerSideLogic({
txt_code: origin_page_content,
@ -46,3 +37,4 @@ async function transformFile(page_path: string) {
createPath: true,
});
}
}