Files
bun.sh/docs/api/Rendering.md
Claude Bot b982fc16d5 Fix React Server Components package references
Correct package names in documentation:
- Use `react-server-dom-bun` instead of `react-server-dom-webpack`
- Update install command to match actual requirements from bake.zig:344
- Fix serverRuntimeImportSource to use correct Bun-specific package

Based on actual implementation in:
- /workspace/bun/src/bake.zig:234 (server_runtime_import)
- /workspace/bun/src/bake.zig:344 (react_install_command)
- /workspace/bun/test/bake/bake-harness.ts (test install commands)

The index.ts file is marked as "unused by Bun itself" and contains
outdated references.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-27 05:40:54 +00:00

18 KiB

Bun Rendering API

The Bun Rendering API is an experimental full-stack rendering system that provides React Server Components support, static site generation, hot module reloading, and framework-agnostic development. It's currently under heavy development and available in canary builds.

⚠️ Warning: The Rendering API is experimental and APIs may change significantly.

Overview

The Bun Rendering API provides:

  • Framework-agnostic architecture with React as a built-in example
  • React Server Components with automatic client/server separation
  • File-based routing with customizable patterns and styles
  • Advanced CSS hot module reloading with framework integration
  • Static Site Generation with dynamic parameter support
  • Hot Module Reloading (HMR) for fast development
  • Production optimization with automatic bundling and minification

Quick Start

Create a bun.app.ts configuration file:

// bun.app.ts
/// <reference path="node_modules/bun/src/bake/bake.d.ts" />

export default {
  port: 3000,
  app: {
    framework: "react", // Built-in React integration
  },
};

Start with:

bun run bun.app.ts

CLI Commands

Development Server

# Run configuration file
bun run bun.app.ts
bun bun.app.ts  # shorthand

Production Build

# Build static site (default)
bun build --app bun.app.ts

# Build with specific entry point
bun build --app ./src/app.tsx

The production build always generates static sites by default, with import.meta.env.STATIC set to true.

Framework Configuration

Built-in React Integration

// bun.app.ts
export default {
  app: {
    framework: "react", // Uses built-in React integration
  },
};

Requires React 19 experimental:

bun add react@experimental react-dom@experimental react-server-dom-bun react-refresh@experimental

The built-in React framework provides:

  • Server Components with automatic client/server boundaries
  • React Fast Refresh for instant feedback
  • CSS hot reloading with framework navigation
  • Static site generation with prerendering

Custom Framework

Bake is designed to be framework-agnostic. Here's how to create a custom framework:

import type { Bake } from "bun";

const customFramework: Bake.Framework = {
  // File-based routing configuration
  fileSystemRouterTypes: [{
    root: "pages",
    style: "nextjs-pages", // or "nextjs-app-ui", "nextjs-app-routes"
    serverEntryPoint: "./server.tsx",
    clientEntryPoint: "./client.tsx",
    layouts: true,
    ignoreUnderscores: true,
    extensions: ["tsx", "jsx"],
  }],
  
  // Static file serving
  staticRouters: ["public"],
  
  // Server Components configuration
  serverComponents: {
    separateSSRGraph: true,
    serverRuntimeImportSource: "react-server-dom-bun/server",
    serverRegisterClientReferenceExport: "registerClientReference",
  },
  
  // Build options
  bundlerOptions: {
    client: {
      conditions: ["browser"],
    },
    server: {
      conditions: ["node"],
    },
    ssr: {
      conditions: ["react-server"],
    },
  },
  
  // Fast Refresh for React
  reactFastRefresh: {
    importSource: "react-refresh/runtime",
  },
  
  // Framework plugins
  plugins: [
    {
      name: "custom-framework-plugin",
      setup(build) {
        // Custom file transformations
        build.onLoad({ filter: /\.custom$/ }, async (args) => {
          const contents = await Bun.file(args.path).text();
          return {
            contents: transformCustomFile(contents),
            loader: "tsx",
          };
        });
      },
    },
  ],
};

export default {
  app: {
    framework: customFramework,
  },
};

Non-React Framework Example (Svelte)

import type { Bake } from "bun";
import * as svelte from "svelte/compiler";

export default function (): Bake.Framework {
  return {
    serverComponents: {
      separateSSRGraph: false,
      serverRuntimeImportSource: "./framework/server.ts",
    },
    fileSystemRouterTypes: [{
      root: "pages",
      serverEntryPoint: "./framework/server.ts",
      clientEntryPoint: "./framework/client.ts",
      style: "nextjs-pages",
      extensions: [".svelte"],
    }],
    plugins: [{
      name: "svelte-server-components",
      setup(build) {
        build.onLoad({ filter: /\.svelte$/ }, async (args) => {
          const contents = await Bun.file(args.path).text();
          const result = svelte.compile(contents, {
            filename: args.path,
            css: "external",
            hmr: true,
            dev: true,
            generate: args.side, // 'server' or 'client'
          });
          
          let jsCode = result.js.code;
          if (result.css) {
            jsCode = `import ${JSON.stringify("svelte-css:" + args.path)};` + jsCode;
          }
          
          return {
            contents: jsCode,
            loader: "js",
            watchFiles: [args.path],
          };
        });
      },
    }],
  };
}

File-Based Routing

Router Styles

Bake supports multiple routing conventions:

Next.js Pages Style ("nextjs-pages")

pages/
├── index.tsx        # /
├── about.tsx        # /about
├── blog/
│   ├── index.tsx    # /blog
│   └── [slug].tsx   # /blog/:slug
└── _layout.tsx      # Layout for all pages

Next.js App Style ("nextjs-app-ui")

app/
├── page.tsx         # /
├── layout.tsx       # Root layout
├── about/
│   └── page.tsx     # /about
└── blog/
    ├── page.tsx     # /blog
    ├── layout.tsx   # Blog layout
    └── [slug]/
        └── page.tsx # /blog/:slug

Custom Router Function

const customRouter: Bake.CustomFileSystemRouterFunction = (path) => {
  if (path.endsWith(".page.tsx")) {
    return {
      pattern: path.replace(/\.page\.tsx$/, ""),
      type: "route",
    };
  }
  if (path.endsWith(".layout.tsx")) {
    return {
      pattern: path.replace(/\.layout\.tsx$/, ""),
      type: "layout",
    };
  }
  return undefined; // Skip file
};

Dynamic Routes

// pages/blog/[slug].tsx
interface Props {
  params: { slug: string };
}

export default function BlogPost({ params }: Props) {
  return <h1>Post: {params.slug}</h1>;
}

// For static site generation
export async function getStaticPaths() {
  const posts = await fetchBlogPosts();
  
  return {
    paths: posts.map(post => ({ params: { slug: post.slug } })),
    exhaustive: true, // All pages generated at build time
  };
}

Layouts

// pages/_layout.tsx (or app/layout.tsx)
interface Props {
  children: React.ReactNode;
  params?: Record<string, string>;
}

export default function RootLayout({ children }: Props) {
  return (
    <html>
      <head>
        <title>My App</title>
      </head>
      <body>
        <nav>Navigation</nav>
        <main>{children}</main>
      </body>
    </html>
  );
}

React Server Components

Server Components (Default)

Server components run on the server and can access databases, APIs, etc:

// pages/posts.tsx - Server Component
async function getPosts() {
  const response = await fetch("https://api.example.com/posts");
  return response.json();
}

export default async function PostsPage() {
  const posts = await getPosts();
  
  return (
    <div>
      <h1>Posts</h1>
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.content}</p>
        </article>
      ))}
    </div>
  );
}

Client Components

Add "use client" for browser-only features:

// components/Counter.tsx - Client Component
"use client";

import { useState } from "react";

export default function Counter() {
  const [count, setCount] = useState(0);
  
  return (
    <button onClick={() => setCount(count + 1)}>
      Count: {count}
    </button>
  );
}

Mixed Usage

// pages/dashboard.tsx - Server Component
import Counter from "../components/Counter"; // Client Component

async function getUser() {
  // Server-side data fetching
  return { name: "John", id: 1 };
}

export default async function Dashboard() {
  const user = await getUser();
  
  return (
    <div>
      <h1>Welcome, {user.name}!</h1>
      {/* This renders on the server */}
      <p>User ID: {user.id}</p>
      
      {/* This adds client-side interactivity */}
      <Counter />
    </div>
  );
}

Static Site Generation

Build Command

bun build --app bun.app.ts

This generates:

  • Static HTML files for each route
  • Optimized JavaScript bundles for client-side code
  • CSS files with automatic optimization
  • RSC payload files (.rsc) for seamless client navigation
  • Source maps for debugging

Dynamic Routes with Parameters

For routes with dynamic segments, you must export a getParams function:

// pages/blog/[slug].tsx
export default function BlogPost({ params }: { params: { slug: string } }) {
  return <h1>Post: {params.slug}</h1>;
}

// Required for static generation of dynamic routes
export async function getParams(): Promise<Bake.GetParamIterator> {
  const posts = await fetchBlogPosts();
  
  return {
    pages: posts.map(post => ({ slug: post.slug })),
    exhaustive: true, // Build will fail if false and route is accessed
  };
}

// Alternative: Next.js compatibility
export async function getStaticPaths() {
  const posts = await fetchBlogPosts();
  
  return {
    paths: posts.map(post => ({ params: { slug: post.slug } })),
    fallback: false, // true = exhaustive: false
  };
}

Streaming Parameter Generation

For large datasets, use async iterators:

export async function* getParams() {
  for await (const batch of fetchPostsInBatches()) {
    for (const post of batch) {
      yield { slug: post.slug };
    }
  }
  return { exhaustive: false }; // More posts may exist
}

Prerendering

Custom server entry points can implement prerendering:

// server.tsx
export async function prerender(meta: Bake.RouteMetadata) {
  // Generate static files
  const html = await renderToStaticHtml(meta);
  const rscPayload = await generateRSCPayload(meta);
  
  return {
    files: {
      "/index.html": html,
      "/index.rsc": rscPayload, // For client navigation
      "/sitemap.xml": generateSitemap(),
    },
  };
}

CSS & Styling

Advanced CSS Hot Module Reloading

Bake features sophisticated CSS HMR that works with any framework:

  • Real-time CSS updates without page reload
  • Framework-aware CSS management during client-side navigation
  • MutationObserver-based tracking of dynamically added/removed styles
  • CSSStyleSheet API for instant style replacement
  • Automatic CSS bundling and optimization
// CSS is automatically hot-reloaded
import "./styles.css";

// CSS modules work seamlessly
import styles from "./component.module.css";

export default function Component() {
  return <div className={styles.container}>Styled component</div>;
}

CSS Chunking and Loading

In production builds:

  • CSS is automatically split by route
  • Critical CSS is inlined
  • Non-critical CSS is loaded asynchronously
  • CSS files are fingerprinted for caching

Development Features

Hot Module Reloading

The Rendering API provides advanced HMR:

  • React Fast Refresh with state preservation
  • CSS hot reloading with instant updates
  • Server-side hot reloading with automatic restart
  • Error overlay with stack traces and source maps
  • File watching with incremental rebuilds

Environment Variables

// Available in all modes
console.log(import.meta.env.MODE); // "development" or "production"
console.log(import.meta.env.SSR);  // true on server, false on client

// Only available in static builds
if (import.meta.env.STATIC) {
  // This code only runs in static builds
  console.log("Building static site");
}

Development Modules

// Available in development
import { onServerSideReload } from "bun:bake/client";

// Hot reload hook for custom frameworks
if (import.meta.env.DEV) {
  onServerSideReload(async () => {
    // Custom reload logic
    await reloadPage();
  });
}

Server Entry Points

Custom Server Implementation

// server.tsx
import type { Bake } from "bun";

export async function render(
  request: Request, 
  meta: Bake.RouteMetadata
): Promise<Response> {
  const { pageModule, layouts, params, styles, modules } = meta;
  
  // Build component tree with layouts
  let route = <pageModule.default params={params} />;
  for (const layout of layouts) {
    const Layout = layout.default;
    route = <Layout params={params}>{route}</Layout>;
  }
  
  // Full HTML document
  const page = (
    <html lang="en">
      <head>
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <title>My App</title>
        {styles.map(url => (
          <link key={url} rel="stylesheet" href={url} data-bake-ssr />
        ))}
      </head>
      <body>
        {route}
        {modules.map(url => (
          <script key={url} type="module" src={url} />
        ))}
      </body>
    </html>
  );
  
  // Render using React Server Components
  return await renderToResponse(page, meta, request);
}

// For static site generation
export async function prerender(meta: Bake.RouteMetadata) {
  return {
    files: {
      "/index.html": await renderToStaticHTML(meta),
      "/index.rsc": await generateRSCPayload(meta),
    },
  };
}

// For dynamic routes in static builds  
export async function getParams(meta: Bake.ParamsMetadata) {
  return {
    pages: await generateParams(meta.pageModule),
    exhaustive: true,
  };
}

Client Entry Point

// client.tsx
import { hydrateRoot } from "react-dom/client";
import { onServerSideReload } from "bun:bake/client";

// Hydrate server-rendered content
// The implementation handles RSC payloads and client navigation automatically

// Hot reload support in development
if (import.meta.env.DEV) {
  onServerSideReload(async () => {
    // Framework can implement custom reload logic
    await navigateToCurrentPage();
  });
}

Production Deployment

Build Output Structure

bun build --app bun.app.ts

Generates in dist/:

dist/
├── index.html              # Static HTML
├── index.rsc               # RSC payload for navigation
├── _bun/
│   ├── client.abc123.js    # Client bundle
│   ├── styles.def456.css   # Styles
│   └── assets/             # Static assets
└── blog/
    ├── hello/
    │   ├── index.html      # Dynamic route: /blog/hello
    │   └── index.rsc
    └── world/
        ├── index.html      # Dynamic route: /blog/world
        └── index.rsc

Deployment Options

Static Hosting

Deploy the dist/ folder to any static host:

# Upload to static hosting
rsync -av dist/ user@server:/var/www/myapp/

Server Deployment

For dynamic features, deploy with Bun:

// production-server.ts
const server = Bun.serve({
  port: process.env.PORT || 3000,
  app: {
    framework: "react",
    bundlerOptions: {
      define: { "process.env.NODE_ENV": '"production"' },
    },
  },
});

API Reference

Rendering Options

interface Options {
  framework: Framework | "react";
  bundlerOptions?: BundlerOptions;
  plugins?: BunPlugin[];
}

RouteMetadata

interface RouteMetadata {
  readonly pageModule: any;                    // Route module
  readonly layouts: ReadonlyArray<any>;        // Layout modules
  readonly params: Record<string, string> | null; // Route parameters
  readonly modules: ReadonlyArray<string>;     // JS files to load
  readonly modulepreload: ReadonlyArray<string>; // Files to preload
  readonly styles: ReadonlyArray<string>;      // CSS files
}

Framework Configuration

interface Framework {
  bundlerOptions?: BundlerOptions;
  fileSystemRouterTypes?: FrameworkFileSystemRouterType[];
  staticRouters?: Array<StaticRouter>;
  builtInModules?: BuiltInModule[];
  serverComponents?: ServerComponentsOptions;
  reactFastRefresh?: boolean | ReactFastRefreshOptions;
  plugins?: BunPlugin[];
}

Special Modules

  • "bun:bake/server" - Server manifest for client components
  • "bun:bake/client" - Client-side reload hooks
  • "bun:bake/dev" - Development utilities

Current Limitations

Since the Rendering API is experimental:

  • ⚠️ APIs may change significantly
  • ⚠️ Limited documentation and examples
  • ⚠️ Requires canary Bun builds
  • ⚠️ React 19 experimental required for React integration
  • ⚠️ No official plugin ecosystem yet

Examples

Custom Svelte Framework

// bun.app.ts
import svelte from "./svelte-framework.ts";

export default {
  app: {
    framework: svelte(),
  },
};

Multi-Framework App

// Support multiple frameworks in one app
export default {
  app: {
    framework: {
      fileSystemRouterTypes: [
        {
          root: "react-pages",
          serverEntryPoint: "./react-server.tsx", 
          clientEntryPoint: "./react-client.tsx",
          style: "nextjs-pages",
          extensions: ["tsx"],
        },
        {
          root: "svelte-pages", 
          serverEntryPoint: "./svelte-server.ts",
          clientEntryPoint: "./svelte-client.ts", 
          style: "nextjs-pages",
          extensions: ["svelte"],
        },
      ],
      plugins: [reactPlugin, sveltePlugin],
    },
  },
};

Advanced Static Site

// pages/blog/[...slug].tsx - Catch-all route
export default function BlogPost({ params }: { params: { slug: string[] } }) {
  const path = params.slug.join('/');
  return <h1>Post: {path}</h1>;
}

export async function getParams() {
  const posts = await fetchAllBlogPosts();
  
  return {
    pages: posts.map(post => ({ 
      slug: post.path.split('/') 
    })),
    exhaustive: true,
  };
}

This documentation reflects the actual implementation of the Bun Rendering API as found in the Bun codebase. The API is experimental and under active development, with React serving as the primary built-in framework example while supporting any framework through the plugin system.