@cmj/juice
v0.9.8
Published
Zero-bloat React 19 RSC framework — streaming SSR, server actions, zero config.
Maintainers
Readme
🧃 Juice
Zero-bloat React 19 RSC framework.
Streaming SSR. Server Actions. Zero config.
Install
npm install @cmj/juice react@^19 react-dom@^19 vite@^6Quickstart
npx @cmj/juice create my-app
cd my-app
npm install
npm run devOr scaffold for a specific target:
npx @cmj/juice create my-app --target cloudflareTargets: bun (default), node, cloudflare, deno
CLI
| Command | Description |
|---------|-------------|
| juice create <name> | Scaffold a new Juice app |
| juice dev | Start the Vite dev server |
| juice build | Production build (client + SSR) |
API
juice/plugin
The Vite plugin. Zero config.
// vite.config.ts
import { defineConfig } from 'vite';
import juice from '@cmj/juice/plugin';
export default defineConfig({
plugins: [juice()],
});juice/runtime
The WinterCG-compliant edge runtime.
// server.ts
import { createRouter } from '@cmj/juice/runtime';
import manifest from './dist/flight-manifest.json' with { type: 'json' };
const handler = createRouter(manifest);
// handler: (Request) => Promise<Response>Server Actions (Two-Argument Pattern)
Simple form actions receive FormData as the first argument — React 19 native. When you need more power, use ActionContext as the opt-in second argument:
'use server';
import type { ActionContext } from '@cmj/juice/runtime';
// Simple: FormData-only (React 19 native)
export async function addToCart(formData: FormData) {
const id = formData.get('productId');
}
// Power: headers, cookies, params via second arg
export async function processWebhook(body: unknown, ctx: ActionContext) {
ctx.request.headers.get('x-signature');
ctx.cookies.get('session_id');
ctx.params.id;
}Test your actions with the public createActionContext factory:
import { createActionContext } from '@cmj/juice/runtime';
const ctx = createActionContext(new Request('https://example.com'));How It Works
Source (.tsx) → Vite Plugin → flight-manifest.json → Runtime → Response- Compile: The Vite plugin detects
'use client'and'use server'directives, generates proxy modules, and emits aflight-manifest.json. - Serve: The runtime reads the manifest and streams RSC responses using React 19's
renderToReadableStream. - Deploy: The
Request → Responsesignature works on any WinterCG platform.
Client-Side Navigation
import { Link } from '@cmj/juice/client';
<Link href="/about">About</Link>
<Link href="/products" prefetch="viewport">Products</Link>
<Link href="/blog" activeClassName="active">Blog</Link>Programmatic navigation:
'use client';
import { useRouter } from '@cmj/juice/client';
const router = useRouter();
router.push('/dashboard');
router.prefetch('/settings');In dev mode, the Vite plugin auto-injects:
- Client hydration —
hydrateRoot()bootstrap souseState,onClick,useEffectwork during development - HMR auto-refresh — Editing routes, components, or CSS triggers instant browser updates
- SPA navigation — Navigation API + View Transitions for smooth page transitions, zero config
CSS & Styling
Import .css files from any component — they're collected and injected as <link> tags automatically.
// app/routes/layout.tsx
import './global.css';CSS modules work out of the box:
import styles from './Button.module.css';
<button className={styles.root}>Click</button>| Environment | Mechanism |
|---|---|
| Dev | Vite module graph walked recursively — <link> tags in <head> |
| Production | CSS assets tracked in flight-manifest.json under the css key |
PostCSS, Sass, Less, Stylus — anything Vite supports works automatically.
Security
Juice ships secure defaults out of the box.
CSRF Protection
All mutating requests (POST, PUT, PATCH, DELETE) validate the Origin header against the Host header. Requests without a matching origin are rejected with 403 Forbidden.
// Enabled by default — no action needed.
const handler = createRouter(manifest);
// Allow specific cross-origin callers:
const handler = createRouter(manifest, {
csrfProtection: {
allowedOrigins: ['https://admin.example.com'],
},
});
// Disable entirely (not recommended):
const handler = createRouter(manifest, { csrfProtection: false });Client Component Isolation
'use client' modules that import Node.js builtins (fs, child_process, net, etc.) fail at build time:
JuiceClientBoundaryError: components/Upload.tsx is a 'use client' module
but imports Node.js builtin "fs". Move this logic to a server action.Security Headers
- Error/404 responses include
Cache-Control: no-storeandContent-Type: text/plain - HEAD requests unconditionally return empty bodies (RFC 9110 §9.3.2)
- CSS bootstrap script injection mitigated via
JSON.stringifyescaping
Features
- Security by default — CSRF protection, client boundary isolation, secure error headers
- React 19 RSC — Server Components, Suspense, streaming SSR
- Server Actions —
'use server'with FormData + opt-inActionContextfor headers, cookies, params - Client hydration in dev —
useState,onClick,useEffectwork during development viahydrateRoot()bootstrap - HMR — Full hot module replacement — CSS instant, TSX full-reload
- SPA navigation —
<Link>component +useRouter()hook + Navigation API bootstrap in dev - CSS pipeline —
import './styles.css', CSS modules, PostCSS, Sass — dev and production - Zero config — One plugin call, auto-discovers routes from
app/routes/ - One dependency — Only
urlpattern-polyfillfor cross-platform routing - Multi-platform — Bun, Node.js, Cloudflare Workers, Deno
- Empathic errors — "What-Why-How" error messages for fast debugging
- Testable actions — Public
createActionContextfactory for unit testing - Dynamic CLI versioning — CLI template always uses the currently installed version
License
MIT
