Set it with `dist_dir` in bunext.config.ts or `--dist-dir` on any CLI command (precedence: --dist-dir > BUNEXT_DIST_DIR > dist_dir > .bunext). setupDistDir() resolves it in the CLI's preAction hook and at the start of bunextInit(), so custom servers get the config value too. - Read dist-derived paths from grabDirNames() inside functions instead of at import time; a guard test enforces this. - Derive the client asset URL prefix from the dist dir instead of hard-coding /.bunext. - Make the dev watcher ignore rules and probe follow the dist dir. - Validate the dir: inside the project root, outside src/, public/ and node_modules/, URL-safe characters only. Co-Authored-By: Claude Opus 5.5 <[email protected]>
4.8 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
About
Bunext is a Next.js-style meta-framework built on Bun and React 19. It provides file-system routing, SSR, HMR, and static site generation, using ESBuild for client-side bundling and Bun.serve() as the HTTP server.
Commands
# Build the TypeScript source (outputs to dist/)
bun run build # tsc
# Watch mode for development of the framework itself
bun run dev # tsc --watch
# CLI commands exposed by the built package (used in consumer projects)
bunext dev # Start dev server with HMR and file watcher
bunext build # Bundle all pages for production
bunext start # Start production server from pre-built artifacts
bunext sitemap # Write public/sitemap.xml and public/robots.txt using config.sitemap / config.robots
Architecture
Entry point
src/index.ts — Shebang CLI (#!/usr/bin/env bun). Uses Commander.js to dispatch dev, build, and start subcommands. Declares all global state (global.CONFIG, global.ROUTER, global.BUNDLER_CTX, global.HMR_CONTROLLERS, etc.).
Command flow
dev: init config → create FileSystemRouter → start ESBuild watch → start FS watcher → start Bun.serve() with HMR WebSocketbuild: init config → run allPagesBundler → exit after first successful buildstart: loadpublic/pages/map.json(pre-built artifact map) → start Bun.serve() without bundler/watcher
Key directories
src/commands/— CLI command implementationssrc/functions/bundler/— ESBuild bundler; uses a virtual namespace plugin to create per-page client hydration entry points and emitsmap.jsonsrc/functions/server/— Server startup, route dispatch, HMR watcher, rebuild logic, web-page rendering pipelinesrc/utils/— Stateless helpers (directory paths, router, config constants, JSON parser, etc.)src/types/— Shared TypeScript typessrc/presets/— Default 404/500 components and samplebunext.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
meta:BunextPageModuleMeta— SEO/OG metadataHead: FC — extra<head>contentconfig:BunextRouteConfig— cache settingshtml_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 export const sitemap:BunextPageSitemapFn— optional; returns the page's sitemap entries (how dynamic routes get intobunext sitemap)
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.
API routes live in src/pages/api/ and follow standard Bun Request → Response handler conventions.
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.
bunext build also updates .bunext/page-lastmod.json (src/utils/page-lastmod-record.ts): per page, a fingerprint of the bundle hash + CSS path and the build date it last changed. The sitemap uses these dates as lastmod.
Config
Consumer projects define bunext.config.ts at their project root. The BunextConfig type fields include dist_dir, assets_prefix, origin, global_vars, port, development.
The dist directory (default .bunext) is resolved by setupDistDir() (src/functions/setup-dist-dir.ts): --dist-dir > BUNEXT_DIST_DIR env > config dist_dir. It runs in the CLI's preAction hook and at the start of bunextInit() (so custom servers get it too), and stores the result in BUNEXT_DIST_DIR, which grabDirNames() reads and child processes inherit. Because it runs after modules are imported, never read a dist-derived path from grabDirNames() at module level — call it inside the function that uses it. Only root-derived paths (ROOT_DIR, PAGES_DIR, PUBLIC_DIR, ...) may be read at module level; src/__tests__/utils/grab-dist-dir.test.ts enforces this. Client bundle URLs are their project-relative paths (grabBunextPublicUrlPath()), so never hard-code /.bunext in URLs or paths.