JSPM

  • Created
  • Published
  • Downloads 63
  • Score
    100M100P100Q70750F
  • License MIT

Zero-bloat React 19 RSC framework โ€” streaming SSR, server actions, zero config.

Package Exports

  • @cmj/juice/client
  • @cmj/juice/create
  • @cmj/juice/package.json
  • @cmj/juice/plugin
  • @cmj/juice/runtime

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@^6

Quickstart

npx @cmj/create-juice-app my-app
cd my-app
npm install
npm run dev

Or scaffold for a specific target:

npx @cmj/create-juice-app my-app --target cloudflare

Targets: 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
  1. Compile: The Vite plugin detects 'use client' and 'use server' directives, generates proxy modules, and emits a flight-manifest.json.
  2. Serve: The runtime reads the manifest and streams RSC responses using React 19's renderToReadableStream.
  3. Deploy: The Request โ†’ Response signature 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 a client bootstrap that intercepts navigations via the Navigation API โ€” SPA transitions with View 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-store and Content-Type: text/plain
  • HEAD requests unconditionally return empty bodies (RFC 9110 ยง9.3.2)
  • CSS bootstrap script injection mitigated via JSON.stringify escaping

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-in ActionContext for headers, cookies, params
  • 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, no magic files
  • One dependency โ€” Only urlpattern-polyfill for cross-platform routing
  • Multi-platform โ€” Bun, Node.js, Cloudflare Workers, Deno
  • Empathic errors โ€” "What-Why-How" error messages for fast debugging
  • HMR โ€” Full hot module replacement in development
  • Testable actions โ€” Public createActionContext factory for unit testing

License

MIT