From 35930857fdad19616460de50bd755a79e4fdd66e Mon Sep 17 00:00:00 2001 From: Benjamin Toby Date: Tue, 17 Mar 2026 19:38:06 +0100 Subject: [PATCH] Update API routes function. Add middleware. Update README.md --- CLAUDE.md | 129 ++-- README.md | 639 +++++++++++++++++- bunfig.toml | 2 - info/how-it-works.md | 132 ---- package.json | 4 +- {commands => src/commands}/build/index.ts | 14 +- {commands => src/commands}/dev/index.ts | 8 +- {commands => src/commands}/start/index.ts | 6 +- src/functions/bundler/all-pages-bundler.ts | 2 +- src/functions/server/handle-routes.ts | 56 +- src/functions/server/server-params-gen.ts | 24 +- src/functions/server/watcher.tsx | 7 - .../server/web-pages/generate-web-html.tsx | 2 +- .../server/web-pages/grab-page-component.tsx | 2 +- index.ts => src/index.ts | 8 +- .../presets/bunext.config.ts | 0 src/types/index.ts | 12 +- src/utils/grab-constants.ts | 8 +- src/utils/grab-route-params.ts | 2 +- tsconfig.json | 2 +- 20 files changed, 747 insertions(+), 312 deletions(-) delete mode 100644 bunfig.toml delete mode 100644 info/how-it-works.md rename {commands => src/commands}/build/index.ts (54%) rename {commands => src/commands}/dev/index.ts (69%) rename {commands => src/commands}/start/index.ts (70%) rename index.ts => src/index.ts (90%) rename bunext.config.ts => src/presets/bunext.config.ts (100%) diff --git a/CLAUDE.md b/CLAUDE.md index 1ee6890..83e5b0a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,106 +1,55 @@ +# CLAUDE.md -Default to using Bun instead of Node.js. +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. -- Use `bun ` instead of `node ` or `ts-node ` -- Use `bun test` instead of `jest` or `vitest` -- Use `bun build ` instead of `webpack` or `esbuild` -- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install` -- Use `bun run - - -``` +### Bundler artifact tracking +`BundlerCTXMap` (stored in `global.BUNDLER_CTX_MAP`) maps each page to its bundled output path, content hash, and entrypoint. In production this is serialized to `public/pages/map.json` and loaded at startup. -With the following `frontend.tsx`: - -```tsx#frontend.tsx -import React from "react"; - -// import .css files directly and it works -import './index.css'; - -import { createRoot } from "react-dom/client"; - -const root = createRoot(document.body); - -export default function Frontend() { - return

Hello, world!

; -} - -root.render(); -``` - -Then, run index.ts - -```sh -bun --hot ./index.ts -``` - -For more information, read the Bun API docs in `node_modules/bun-types/docs/**.md`. +### Config +Consumer projects define `bunext.config.ts` at their project root. The `BunextConfig` type fields: `distDir`, `assetsPrefix`, `origin`, `globalVars`, `port`, `development`. diff --git a/README.md b/README.md index 90b9436..3ada3ec 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,636 @@ # Bunext -A Next JS replacement built with bun JS and docker +A Next.js-style full-stack meta-framework built on [Bun](https://bun.sh) and React 19. Bunext provides server-side rendering, file-system based routing, Hot Module Replacement (HMR), and client-side hydration — using ESBuild to bundle client assets and Bun's native HTTP server (`Bun.serve`) to handle requests. -## Running this application +--- -To run development: +## Table of Contents -```bash -bun dev -``` +- [Requirements](#requirements) +- [Installation](#installation) +- [Quick Start](#quick-start) +- [CLI Commands](#cli-commands) +- [Project Structure](#project-structure) +- [File-System Routing](#file-system-routing) +- [Pages](#pages) + - [Basic Page](#basic-page) + - [Server Function](#server-function) + - [Redirects from Server](#redirects-from-server) + - [Custom Response Options](#custom-response-options) + - [SEO Metadata](#seo-metadata) + - [Dynamic Metadata](#dynamic-metadata) + - [Custom Head Content](#custom-head-content) + - [Root Layout](#root-layout) +- [API Routes](#api-routes) + - [Route Config (Body Size Limit)](#route-config-body-size-limit) +- [Error Pages](#error-pages) +- [Static Files](#static-files) +- [Configuration](#configuration) + - [Middleware](#middleware) +- [Environment Variables](#environment-variables) +- [How It Works](#how-it-works) + - [Development Server](#development-server) + - [Production Build](#production-build) + - [Bundler](#bundler) + - [Hot Module Replacement](#hot-module-replacement) + - [Request Pipeline](#request-pipeline) -To run production: - -```bash -bun start -``` +--- ## Requirements -### Docker +- [Bun](https://bun.sh) v1.0 or later +- TypeScript 5.0+ +- React 19 and react-dom 19 (peer dependencies) -You need `docker` installed to run this project +--- + +## Installation + +Install Bunext as a dependency in your project: + +```bash +bun add bunext +``` + +Install required peer dependencies: + +```bash +bun add react react-dom +bun add -d typescript @types/react @types/react-dom +``` + +--- + +## Quick Start + +1. Create a minimal project layout: + +``` +my-app/ +├── src/ +│ └── pages/ +│ └── index.tsx +├── bunext.config.ts # optional +└── package.json +``` + +2. Create your first page (`src/pages/index.tsx`): + +```tsx +export default function HomePage() { + return

Hello from Bunext!

; +} +``` + +3. Start the development server: + +```bash +bunext dev +``` + +4. Open `http://localhost:7000` in your browser. + +--- + +## CLI Commands + +| Command | Description | +| -------------- | ---------------------------------------------------------------------- | +| `bunext dev` | Start the development server with HMR and file watching. | +| `bunext build` | Bundle all pages for production. Outputs artifacts to `public/pages/`. | +| `bunext start` | Start the production server using pre-built artifacts. | + +```bash +# Development +bunext dev + +# Production build +bunext build + +# Production server (must run build first) +bunext start +``` + +> **Note:** `bunext start` will exit with an error if `public/pages/map.json` does not exist. Always run `bunext build` before `bunext start`. + +--- + +## Project Structure + +A typical Bunext project has the following layout: + +``` +my-app/ +├── src/ +│ └── pages/ # File-system routes (pages and API handlers) +│ ├── __root.tsx # Optional: root layout wrapper for all pages +│ ├── index.tsx # Route: / +│ ├── about.tsx # Route: /about +│ ├── 404.tsx # Optional: custom 404 page +│ ├── 500.tsx # Optional: custom 500 page +│ ├── blog/ +│ │ ├── index.tsx # Route: /blog +│ │ └── [slug].tsx # Route: /blog/:slug (dynamic) +│ └── api/ +│ └── users.ts # API route: /api/users +├── public/ # Static files and bundler output +│ └── pages/ # Generated by bundler (do not edit manually) +│ └── map.json # Artifact map used by production server +├── bunext.config.ts # Optional configuration +├── tsconfig.json +└── package.json +``` + +--- + +## File-System Routing + +Bunext uses `Bun.FileSystemRouter` with Next.js-style routing. Pages live in `src/pages/` and are automatically mapped to URL routes: + +| File path | URL path | +| -------------------------------- | ------------- | +| `src/pages/index.tsx` | `/` | +| `src/pages/about.tsx` | `/about` | +| `src/pages/blog/index.tsx` | `/blog` | +| `src/pages/blog/[slug].tsx` | `/blog/:slug` | +| `src/pages/users/[id]/index.tsx` | `/users/:id` | +| `src/pages/api/users.ts` | `/api/users` | + +Dynamic route parameters (e.g. `[slug]`) are available in the `server` function via `ctx.req.url` or from the `query` field in the server response. + +--- + +## Pages + +### Basic Page + +A page file must export a default React component. The component receives server-side props automatically. + +```tsx +// src/pages/index.tsx +export default function HomePage() { + return

Hello, World!

; +} +``` + +### Server Function + +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. + +```tsx +// src/pages/profile.tsx +import type { BunextPageServerFn } from "bunext/src/types"; + +type Props = { + props?: { username: string; bio: string }; + query?: Record; +}; + +export const server: BunextPageServerFn<{ + username: string; + bio: string; +}> = async (ctx) => { + // ctx.req — the raw Request object + // ctx.url — the parsed URL + // ctx.query — query string parameters + // ctx.resTransform — optional response interceptor + + const username = "alice"; + const bio = "Software engineer"; + + return { + props: { username, bio }, + }; +}; + +export default function ProfilePage({ props }: Props) { + return ( +
+

{props?.username}

+

{props?.bio}

+
+ ); +} +``` + +The server function receives a `ctx` object (type `BunxRouteParams`) with: + +| Field | Type | Description | +| -------------- | ------------------------------------------------ | ------------------------------------------ | +| `req` | `Request` | Raw Bun/Web Request object | +| `url` | `URL` | Parsed URL | +| `body` | `any` | Parsed request body | +| `query` | `any` | Query string parameters | +| `resTransform` | `(res: Response) => Promise\|Response` | Intercept and transform the final response | + +### Redirects from Server + +Return a `redirect` object from the `server` function to redirect the client: + +```tsx +export const server: BunextPageServerFn = async (ctx) => { + const isLoggedIn = false; // check auth + + if (!isLoggedIn) { + return { + redirect: { + destination: "/login", + permanent: false, // uses 302 + // status_code: 307 // override status code + }, + }; + } + + return { props: {} }; +}; +``` + +`permanent: true` sends a `301` redirect. Otherwise it sends `302`, or the value of `status_code` if provided. + +### Custom Response Options + +Control status codes, headers, and other response options from the server function: + +```tsx +export const server: BunextPageServerFn = async (ctx) => { + return { + props: { message: "Created" }, + responseOptions: { + status: 201, + headers: { + "X-Custom-Header": "my-value", + }, + }, + }; +}; +``` + +### SEO Metadata + +Export a `meta` object to inject SEO and Open Graph tags into the ``: + +```tsx +import type { BunextPageModuleMeta } from "bunext/src/types"; + +export const meta: BunextPageModuleMeta = { + title: "My Page Title", + description: "A description for search engines.", + keywords: ["bun", "react", "ssr"], + author: "Alice", + robots: "index, follow", + canonical: "https://example.com/about", + themeColor: "#ffffff", + og: { + title: "My Page Title", + description: "Shared on social media.", + image: "https://example.com/og-image.png", + url: "https://example.com/about", + type: "website", + siteName: "My Site", + locale: "en_US", + }, + twitter: { + card: "summary_large_image", + title: "My Page Title", + description: "Shared on Twitter.", + image: "https://example.com/twitter-image.png", + site: "@mysite", + creator: "@alice", + }, +}; + +export default function AboutPage() { + return

About us

; +} +``` + +### Dynamic Metadata + +`meta` can also be an async function that receives the request context and server response: + +```tsx +import type { BunextPageModuleMetaFn } from "bunext/src/types"; + +export const meta: BunextPageModuleMetaFn = async ({ ctx, serverRes }) => { + return { + title: `Post: ${serverRes?.props?.title ?? "Untitled"}`, + description: serverRes?.props?.excerpt, + }; +}; +``` + +### Custom Head Content + +Export a `Head` functional component to inject arbitrary HTML into ``. It receives the server response and request context: + +```tsx +import type { BunextPageHeadFCProps } from "bunext/src/types"; + +export function Head({ serverRes, ctx }: BunextPageHeadFCProps) { + return ( + <> + + + + ); +} + +export default function Page() { + return
Content
; +} +``` + +### 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: + +```tsx +// src/pages/__root.tsx +import type { PropsWithChildren } from "react"; + +export default function RootLayout({ + children, + props, +}: PropsWithChildren) { + return ( + <> +
My App
+
{children}
+
© 2025
+ + ); +} +``` + +--- + +## API Routes + +Create files under `src/pages/api/` to define API endpoints. The default export receives a `BunxRouteParams` object and must return a standard `Response`. + +```ts +// src/pages/api/hello.ts +import type { BunxRouteParams } from "bunext/src/types"; + +export default async function handler(ctx: BunxRouteParams): Promise { + return Response.json({ message: "Hello from the API" }); +} +``` + +API routes are matched at `/api/`. Because the handler returns a plain `Response`, you control the status code, headers, and body format entirely: + +```ts +// src/pages/api/users.ts +import type { BunxRouteParams } from "bunext/src/types"; + +export default async function handler(ctx: BunxRouteParams): Promise { + if (ctx.req.method !== "GET") { + return Response.json({ error: "Method not allowed" }, { status: 405 }); + } + + const users = [{ id: 1, name: "Alice" }]; + + return Response.json({ success: true, data: users }); +} +``` + +The `ctx` parameter has the same shape as the page `server` function context — see the [Server Function](#server-function) section for the full field reference. The `ctx.server` field additionally exposes the Bun `Server` instance. + +### Route Config (Body Size Limit) + +Export a `config` object to override the per-route request body limit (default: 10 MB): + +```ts +import type { BunextServerRouteConfig, BunxRouteParams } from "bunext/src/types"; + +export const config: BunextServerRouteConfig = { + maxRequestBodyMB: 50, // allow up to 50 MB +}; + +export default async function handler(ctx: BunxRouteParams): Promise { + // handle large uploads + return Response.json({ success: true }); +} +``` + +--- + +## Error Pages + +Bunext serves built-in fallback error pages, but you can override them by creating pages in `src/pages/`: + +| File | Triggered when | +| ------------------- | --------------------------------------- | +| `src/pages/404.tsx` | No matching route is found | +| `src/pages/500.tsx` | An unhandled error occurs during render | + +The error message is passed as `children` to the component: + +```tsx +// src/pages/404.tsx +import type { PropsWithChildren } from "react"; + +export default function NotFoundPage({ children }: PropsWithChildren) { + return ( +
+

404 — Page Not Found

+

{children}

+
+ ); +} +``` + +If no custom error pages exist, Bunext uses built-in preset components. + +--- + +## Static Files + +Place static files in the `public/` directory. They are served at the `/public/` URL path: + +``` +public/ +├── logo.png → http://localhost:7000/public/logo.png +├── styles.css → http://localhost:7000/public/styles.css +└── favicon.ico → http://localhost:7000/favicon.ico +``` + +> Favicons are also served from `/public/` but matched directly at `/favicon.*`. + +--- + +## Configuration + +Create a `bunext.config.ts` file in your project root to configure Bunext: + +```ts +// bunext.config.ts +import type { BunextConfig } from "bunext/src/types"; + +const config: BunextConfig = { + port: 3000, // default: 7000 + origin: "https://example.com", + distDir: ".bunext", // directory for internal build artifacts + assetsPrefix: "_bunext/static", + globalVars: { + MY_API_URL: "https://api.example.com", + }, + development: false, // forced by the CLI; set manually if needed +}; + +export default config; +``` + +| Option | Type | Default | Description | +| -------------- | -------------------------------------------------------------------- | ---------------- | -------------------------------------------------- | +| `port` | `number` | `7000` | HTTP server port | +| `origin` | `string` | — | Canonical origin URL | +| `distDir` | `string` | `.bunext` | Internal artifact directory | +| `assetsPrefix` | `string` | `_bunext/static` | URL prefix for static assets | +| `globalVars` | `{ [k: string]: any }` | — | Variables injected globally at build time | +| `development` | `boolean` | — | Overridden to `true` by `bunext dev` automatically | +| `middleware` | `(params: BunextConfigMiddlewareParams) => Response \| undefined \| Promise<...>` | — | Global middleware — see [Middleware](#middleware) | + +### Middleware + +Middleware runs on every request before any routing. Define it in `bunext.config.ts` via the `middleware` field. The function receives `{ req, url, server }` (type `BunextConfigMiddlewareParams`). + +- If it returns a `Response`, that response is sent to the client immediately and no further routing occurs. +- If it returns `undefined` (or nothing), the request proceeds normally through the router. + +```ts +// bunext.config.ts +import type { BunextConfig, BunextConfigMiddlewareParams } from "bunext/src/types"; + +const config: BunextConfig = { + middleware: async ({ req, url, server }) => { + // Example: protect all /dashboard/* routes + if (url.pathname.startsWith("/dashboard")) { + const token = req.headers.get("authorization"); + + if (!token) { + return Response.redirect("/login", 302); + } + } + + // Return undefined to continue to the normal request pipeline + return undefined; + }, +}; + +export default config; +``` + +The middleware can return any valid `Response`, including redirects, JSON, or HTML: + +```ts +middleware: async ({ req, url }) => { + // Block a specific path + if (url.pathname === "/maintenance") { + return new Response("Down for maintenance", { status: 503 }); + } + + // Add CORS headers to all API responses + if (url.pathname.startsWith("/api/")) { + // Let the request proceed, but transform the response via resTransform on individual routes + } +}, +``` + +--- + +## Environment Variables + +| Variable | Description | +| -------- | ------------------------------------------------------- | +| `PORT` | Override the server port (takes precedence over config) | + +--- + +## How It Works + +### Development Server + +Running `bunext dev`: + +1. Loads `bunext.config.ts` and sets `development: true`. +2. Initializes directories (`.bunext/`, `public/pages/`). +3. Creates a `Bun.FileSystemRouter` pointed at `src/pages/`. +4. Starts the ESBuild bundler in **watch mode** — it will automatically rebuild when file content changes. +5. Starts a file-system watcher on `src/` — when a file is created or deleted (a "rename" event), it triggers a full bundler rebuild to update the entry points. +6. Waits for the first successful bundle. +7. Starts `Bun.serve()`. + +### Production Build + +Running `bunext build`: + +1. Sets `NODE_ENV=production`. +2. Runs ESBuild once (not in watch mode) with minification enabled. +3. Writes all bundled artifacts to `public/pages/` and the artifact map to `public/pages/map.json`. +4. Exits. + +### Production Server + +Running `bunext start`: + +1. Reads `public/pages/map.json` to load the pre-built artifact map. +2. Starts `Bun.serve()` without any bundler or file watcher. + +### Bundler + +The bundler (`allPagesBundler`) uses ESBuild with three custom plugins: + +- **`tailwindcss` plugin** — Processes any `.css` files through PostCSS + Tailwind CSS before bundling. +- **`virtual-entrypoints` plugin** — Generates an in-memory client hydration entry point for each page. Each entry imports the page component and calls `hydrateRoot()` against the server-rendered DOM node. If `src/pages/__root.tsx` exists, the page is wrapped in the root layout. +- **`artifact-tracker` plugin** — After each build, collects all output file paths, content hashes, and source entrypoints into a `BundlerCTXMap[]`. This map is stored in `global.BUNDLER_CTX_MAP` and written to `public/pages/map.json`. + +Output files are named `[dir]/[name]/[hash]` so filenames change when content changes, enabling cache-busting. + +### Hot Module Replacement + +In development, Bunext injects a script into every HTML page that opens a **Server-Sent Events (SSE)** connection to `/__hmr`. When a rebuild completes and the bundle hash for a page changes, the server pushes an event through the SSE stream and the client reloads the page automatically. + +The SSE controller for each connected client is tracked in `global.HMR_CONTROLLERS`, keyed by the page URL and its bundled artifact map entry. On disconnect, the controller is removed from the list. + +### Request Pipeline + +Every incoming request is handled by `Bun.serve()` and routed as follows: + +``` +Request + │ + ├── config.middleware({ req, url, server }) + │ Returns Response? → short-circuit, send response immediately + │ Returns undefined → continue + │ + ├── GET /__hmr → Open SSE stream for HMR (dev only) + │ + ├── /api/* → API route handler + │ Matches src/pages/api/.ts + │ Checks content-length against maxRequestBodyMB / 10 MB default + │ Calls module.default(ctx) → returns Response directly + │ + ├── /public/* → Serve static file from public/ + │ + ├── /favicon.* → Serve favicon from public/ + │ + └── Everything else → Server-side render a page + 1. Match route via FileSystemRouter + 2. Find bundled artifact in BUNDLER_CTX_MAP + 3. Import page module (with cache-busting timestamp in dev) + 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 - - -
- - - - - -The pageProps (from server()) are serialized via EJSON and injected as window.**BUNEXT_PAGE_PROPS** so the client -hydration script can read them without an extra network request. - ---- - -6. Client Hydration (browser) - -The browser: - -1. Parses the server-rendered HTML and displays it immediately (no blank flash). -2. Loads /public/pages/index.js (the Bun-bundled client script). -3. That script calls hydrateRoot(container, ) — React attaches event handlers - to the existing DOM rather than re-rendering from scratch. - -At this point the page is fully interactive. - ---- - -7. HMR (dev only) - -The injected EventSource("/\_\_hmr") maintains a persistent SSE connection. When the watcher detects a file change, it -rebuilds all pages, updates LAST_BUILD_TIME, and sends event: update\ndata: reload down the SSE stream. The browser -calls window.location.reload(), which re-requests the page and repeats steps 4–6 with the fresh module. diff --git a/package.json b/package.json index b482427..7669c28 100644 --- a/package.json +++ b/package.json @@ -12,9 +12,7 @@ ], "scripts": { "dev": "tsc --watch", - "docker:dev": "cd envs/development && docker compose down && docker compose up --build", - "start": "cd envs/production && docker compose down && docker compose up -d --build", - "preview": "cd envs/preview && docker compose down && docker compose up -d --build" + "build": "tsc" }, "devDependencies": { "@types/bun": "latest", diff --git a/commands/build/index.ts b/src/commands/build/index.ts similarity index 54% rename from commands/build/index.ts rename to src/commands/build/index.ts index 5d49cb4..8ebaf38 100644 --- a/commands/build/index.ts +++ b/src/commands/build/index.ts @@ -1,8 +1,8 @@ import { Command } from "commander"; -import grabConfig from "../../src/functions/grab-config"; -import init from "../../src/functions/init"; -import type { BunextConfig } from "../../src/types"; -import allPagesBundler from "../../src/functions/bundler/all-pages-bundler"; +import grabConfig from "../../functions/grab-config"; +import init from "../../functions/init"; +import type { BunextConfig } from "../../types"; +import allPagesBundler from "../../functions/bundler/all-pages-bundler"; export default function () { return new Command("build") @@ -23,12 +23,6 @@ export default function () { allPagesBundler({ exit_after_first_build: true, - // async post_build_fn({ artifacts }) { - // writeFileSync( - // HYDRATION_DST_DIR_MAP_JSON_FILE, - // JSON.stringify(artifacts), - // ); - // }, }); }); } diff --git a/commands/dev/index.ts b/src/commands/dev/index.ts similarity index 69% rename from commands/dev/index.ts rename to src/commands/dev/index.ts index bad8d17..952b8b6 100644 --- a/commands/dev/index.ts +++ b/src/commands/dev/index.ts @@ -1,8 +1,8 @@ import { Command } from "commander"; -import grabConfig from "../../src/functions/grab-config"; -import startServer from "../../src/functions/server/start-server"; -import init from "../../src/functions/init"; -import type { BunextConfig } from "../../src/types"; +import grabConfig from "../../functions/grab-config"; +import startServer from "../../functions/server/start-server"; +import init from "../../functions/init"; +import type { BunextConfig } from "../../types"; export default function () { return new Command("dev") diff --git a/commands/start/index.ts b/src/commands/start/index.ts similarity index 70% rename from commands/start/index.ts rename to src/commands/start/index.ts index 4b98854..1389067 100644 --- a/commands/start/index.ts +++ b/src/commands/start/index.ts @@ -1,7 +1,7 @@ import { Command } from "commander"; -import grabConfig from "../../src/functions/grab-config"; -import startServer from "../../src/functions/server/start-server"; -import init from "../../src/functions/init"; +import grabConfig from "../../functions/grab-config"; +import startServer from "../../functions/server/start-server"; +import init from "../../functions/init"; export default function () { return new Command("start") diff --git a/src/functions/bundler/all-pages-bundler.ts b/src/functions/bundler/all-pages-bundler.ts index 1345b9d..a6619eb 100644 --- a/src/functions/bundler/all-pages-bundler.ts +++ b/src/functions/bundler/all-pages-bundler.ts @@ -41,7 +41,7 @@ type Params = { export default async function allPagesBundler(params?: Params) { const pages = grabAllPages({ exclude_api: true }); const { ClientRootElementIDName, ClientRootComponentWindowName } = - await grabConstants(); + grabConstants(); const virtualEntries: Record = {}; const dev = isDevelopment(); diff --git a/src/functions/server/handle-routes.ts b/src/functions/server/handle-routes.ts index 2d82811..4707e2d 100644 --- a/src/functions/server/handle-routes.ts +++ b/src/functions/server/handle-routes.ts @@ -1,9 +1,5 @@ import type { Server } from "bun"; -import type { - APIResponseObject, - BunextServerRouteConfig, - BunxRouteParams, -} from "../../types"; +import type { BunextServerRouteConfig, BunxRouteParams } from "../../types"; import grabRouteParams from "../../utils/grab-route-params"; import grabConstants from "../../utils/grab-constants"; import grabRouter from "../../utils/grab-router"; @@ -13,14 +9,10 @@ type Params = { server: Server; }; -export default async function ({ - req, - server, -}: Params): Promise { +export default async function ({ req, server }: Params): Promise { const url = new URL(req.url); - const { MBInBytes, ServerDefaultRequestBodyLimitBytes } = - await grabConstants(); + const { MBInBytes, ServerDefaultRequestBodyLimitBytes } = grabConstants(); const router = grabRouter(); @@ -28,13 +20,19 @@ export default async function ({ if (!match?.filePath) { const errMsg = `Route ${url.pathname} not found`; - // console.error(errMsg); - return { - success: false, - status: 401, - msg: errMsg, - }; + return Response.json( + { + success: false, + msg: errMsg, + }, + { + status: 401, + headers: { + "Content-Type": "application/json", + }, + }, + ); } const routeParams: BunxRouteParams = await grabRouteParams({ req }); @@ -52,17 +50,25 @@ export default async function ({ size > config.maxRequestBodyMB * MBInBytes) || size > ServerDefaultRequestBodyLimitBytes ) { - return { - success: false, - status: 413, - msg: "Request Body Too Large!", - }; + return Response.json( + { + success: false, + msg: "Request Body Too Large!", + }, + { + status: 413, + headers: { + "Content-Type": "application/json", + }, + }, + ); } } - const res: APIResponseObject = await module["default"]( - routeParams as BunxRouteParams, - ); + const res: Response = await module["default"]({ + ...routeParams, + server, + } as BunxRouteParams); return res; } diff --git a/src/functions/server/server-params-gen.ts b/src/functions/server/server-params-gen.ts index 2da6dff..e31d797 100644 --- a/src/functions/server/server-params-gen.ts +++ b/src/functions/server/server-params-gen.ts @@ -5,6 +5,7 @@ import grabDirNames from "../../utils/grab-dir-names"; import handleWebPages from "./web-pages/handle-web-pages"; import handleRoutes from "./handle-routes"; import isDevelopment from "../../utils/is-development"; +import grabConstants from "../../utils/grab-constants"; type Params = { dev?: boolean; @@ -19,6 +20,20 @@ export default async function (params?: Params): Promise { try { const url = new URL(req.url); + const { config } = grabConstants(); + + if (config?.middleware) { + const middleware_res = await config.middleware({ + req, + url, + server, + }); + + if (typeof middleware_res == "object") { + return middleware_res; + } + } + if (url.pathname === "/__hmr" && isDevelopment()) { const referer_url = new URL( req.headers.get("referer") || "", @@ -69,14 +84,7 @@ export default async function (params?: Params): Promise { } if (url.pathname.startsWith("/api/")) { - const res = await handleRoutes({ req, server }); - - return new Response(JSON.stringify(res), { - status: res?.status, - headers: { - "Content-Type": "application/json", - }, - }); + return await handleRoutes({ req, server }); } if (url.pathname.startsWith("/public/")) { diff --git a/src/functions/server/watcher.tsx b/src/functions/server/watcher.tsx index 6f5f356..d072b12 100644 --- a/src/functions/server/watcher.tsx +++ b/src/functions/server/watcher.tsx @@ -5,8 +5,6 @@ import rebuildBundler from "./rebuild-bundler"; const { SRC_DIR } = grabDirNames(); -const PAGE_FILE_RE = /\.(tsx?|jsx?|css)$/; - export default function watcher() { watch( SRC_DIR, @@ -16,12 +14,7 @@ export default function watcher() { }, async (event, filename) => { if (!filename) return; - const file_path = path.join(SRC_DIR, filename); - // if (!PAGE_FILE_RE.test(filename)) return; - // "change" events (file content modified) are already handled by - // esbuild's internal ctx.watch(). Only "rename" (create or delete) - // requires a full rebuild because entry points have changed. if (event !== "rename") return; if (global.RECOMPILING) return; diff --git a/src/functions/server/web-pages/generate-web-html.tsx b/src/functions/server/web-pages/generate-web-html.tsx index 1777c7a..27048a8 100644 --- a/src/functions/server/web-pages/generate-web-html.tsx +++ b/src/functions/server/web-pages/generate-web-html.tsx @@ -16,7 +16,7 @@ export default async function genWebHTML({ routeParams, }: LivePageDistGenParams) { const { ClientRootElementIDName, ClientWindowPagePropsName } = - await grabContants(); + grabContants(); const { renderToString } = await import( path.join(process.cwd(), "node_modules", "react-dom", "server") diff --git a/src/functions/server/web-pages/grab-page-component.tsx b/src/functions/server/web-pages/grab-page-component.tsx index f0a2047..fb5fcb6 100644 --- a/src/functions/server/web-pages/grab-page-component.tsx +++ b/src/functions/server/web-pages/grab-page-component.tsx @@ -125,7 +125,7 @@ export default async function grabPageComponent({ : undefined; const Component = module.default as FC; - const Head = module.head as FC; + const Head = module.Head as FC; const component = RootComponent ? ( diff --git a/index.ts b/src/index.ts similarity index 90% rename from index.ts rename to src/index.ts index 30e4ecf..3bd6dce 100755 --- a/index.ts +++ b/src/index.ts @@ -8,12 +8,12 @@ import type { BundlerCTXMap, BunextConfig, GlobalHMRControllerObject, -} from "./src/types"; +} from "./types"; import type { FileSystemRouter, Server } from "bun"; -import init from "./src/functions/init"; -import grabDirNames from "./src/utils/grab-dir-names"; +import init from "./functions/init"; +import grabDirNames from "./utils/grab-dir-names"; import build from "./commands/build"; -import type { BuildContext, BuildResult } from "esbuild"; +import type { BuildContext } from "esbuild"; /** * # Declare Global Variables diff --git a/bunext.config.ts b/src/presets/bunext.config.ts similarity index 100% rename from bunext.config.ts rename to src/presets/bunext.config.ts diff --git a/src/types/index.ts b/src/types/index.ts index bdc22ee..45ff930 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -48,6 +48,15 @@ export type BunextConfig = { globalVars?: { [k: string]: any }; port?: number; development?: boolean; + middleware?: ( + params: BunextConfigMiddlewareParams, + ) => Promise | Response | undefined; +}; + +export type BunextConfigMiddlewareParams = { + req: Request; + url: URL; + server: Server; }; export type GetRouteReturn = { @@ -69,6 +78,7 @@ export type BunxRouteParams = { * Intercept and Transform the response object */ resTransform?: (res: Response) => Promise | Response; + server?: Server; }; export interface PostInsertReturn { @@ -146,7 +156,7 @@ export type BunextPageModule = { default: FC; server?: BunextPageServerFn; meta?: BunextPageModuleMeta | BunextPageModuleMetaFn; - head?: FC; + Head?: FC; }; export type BunextPageModuleMetaFn = (params: { diff --git a/src/utils/grab-constants.ts b/src/utils/grab-constants.ts index 9ffb168..f61886e 100644 --- a/src/utils/grab-constants.ts +++ b/src/utils/grab-constants.ts @@ -1,8 +1,5 @@ -import path from "path"; -import grabConfig from "../functions/grab-config"; - -export default async function grabConstants() { - const config = await grabConfig(); +export default function grabConstants() { + const config = global.CONFIG; const MB_IN_BYTES = 1024 * 1024; const ClientWindowPagePropsName = "__PAGE_PROPS__"; @@ -20,5 +17,6 @@ export default async function grabConstants() { ServerDefaultRequestBodyLimitBytes, ClientRootComponentWindowName, MaxBundlerRebuilds, + config, } as const; } diff --git a/src/utils/grab-route-params.ts b/src/utils/grab-route-params.ts index 0acc5d9..956f36c 100644 --- a/src/utils/grab-route-params.ts +++ b/src/utils/grab-route-params.ts @@ -1,4 +1,3 @@ -import type { Server } from "bun"; import type { BunxRouteParams } from "../types"; import deserializeQuery from "./deserialize-query"; @@ -26,6 +25,7 @@ export default async function grabRouteParams({ url, query, body, + server: global.SERVER, }; return routeParams; diff --git a/tsconfig.json b/tsconfig.json index 5e24ac3..c0d18c2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,6 +17,6 @@ "noPropertyAccessFromIndexSignature": false, "outDir": "dist" }, - "include": ["src", "commands", "index.ts"], + "include": ["src", "commands", "src/index.ts"], "exclude": ["node_modules", "dist"] }