/SKILL.md
Bun-native web framework with file routing. Use this for building server-rendered pages with client mount scripts for interactivity.
npx skillsauth add 7flash/melina.js tradjsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
TradJS is a Bun-native web framework. Pages are server-rendered JSX, client interactivity is added via mount scripts — .client.tsx files compiled to lightweight VNodes with a ~2KB reconciler runtime.
Current version: 2.5.8
Use TradJS when the user wants:
TradJS replaces the full Next.js / Vite stack — server pages, API routes, SSG, and client interactivity — with zero React on the client.
mkdir my-app && cd my-app
bun init -y
bun add tradjs
Create server.ts:
import { serve } from 'tradjs';
await serve({
port: parseInt(process.env.BUN_PORT || "3000"),
defaultTitle: 'My App',
});
Create tsconfig.json:
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "tradjs/client",
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"noEmit": true,
"skipLibCheck": true
},
"include": ["app/**/*"]
}
Critical:
"jsxImportSource": "tradjs/client"is required. This tells TypeScript and Bun to use TradJS's VDOM runtime for JSX instead of React.
When building examples inside the tradjs repo, import from local source:
// server.ts (inside examples/)
import { start } from '../../src/web';
And use paths in tsconfig.json:
{
"compilerOptions": {
"paths": {
"tradjs/client/*": ["../../src/client/*"],
"tradjs/*": ["../../src/*"]
}
}
}
my-app/
├── app/
│ ├── layout.tsx # Root layout (wraps all pages)
│ ├── layout.client.tsx # Persistent client mount script
│ ├── globals.css # Global styles (auto-discovered)
│ ├── page.tsx # Home page (/)
│ ├── page.client.tsx # Home page mount script
│ ├── page.css # Scoped CSS for home page
│ ├── middleware.ts # Root middleware
│ ├── error.tsx # Error boundary
│ ├── about/
│ │ └── page.tsx # /about
│ ├── post/[id]/
│ │ ├── page.tsx # /post/:id (dynamic route)
│ │ └── page.client.tsx # Client interactivity for post
│ └── api/
│ └── messages/
│ └── route.ts # API: GET/POST /api/messages
├── server.ts
├── tsconfig.json
└── package.json
| File Pattern | URL | Type |
|---|---|---|
| app/page.tsx | / | Page |
| app/about/page.tsx | /about | Page |
| app/post/[id]/page.tsx | /post/:id | Dynamic page |
| app/api/messages/route.ts | /api/messages | API route |
| app/layout.tsx | — | Layout (wraps children) |
| app/middleware.ts | — | Runs before page render |
| app/error.tsx | — | Catches render errors |
globals.css, global.css, or app.css → processed with PostCSS + Tailwind v4page.css or style.css → scoped CSS for that route segmentlayout.tsx → layout wrapping child pages (nested layouts compose automatically)layout.client.tsx → persistent client mount (survives navigations)page.client.tsx → per-page transient client mountmiddleware.ts → runs root→leaf before page rendererror.tsx → catches render errors with full layout chromepage.tsx)Pages run on the server. They can access databases, read files, anything server-side. They return JSX rendered to HTML via renderToString.
// app/page.tsx
export default function Page() {
return (
<div>
<h1>Welcome</h1>
<p>Server-rendered at {new Date().toISOString()}</p>
<div id="app-root" />
</div>
);
}
Important patterns:
id="" attributes on containers that client scripts will targetlayout.tsx)Root layout wraps all pages. Must include {children}.
// app/layout.tsx
export default function RootLayout({ children }: { children: any }) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>My App</title>
</head>
<body>
<nav><a href="/">Home</a> | <a href="/about">About</a></nav>
<main id="melina-page-content">{children}</main>
</body>
</html>
);
}
Key points:
{children} is where the current page rendersid="melina-page-content" enables targeted page swaps during navigationlayout.tsx in any subdirectory<Head> ComponentDeclarative per-page <head> management:
// app/features/about/page.tsx
import { Head } from 'melina/web';
export default function AboutPage() {
return (
<>
<Head>
<title>About Us — My App</title>
<meta name="description" content="Learn about our team" />
<link rel="canonical" href="https://example.com/about" />
</Head>
<main><h1>About Us</h1></main>
</>
);
}
Head elements are collected during SSR and injected into <head>.
page.client.tsx)Melina uses a pure VDOM architecture for client interactivity. No hooks, no signals. Call render(vnode, container) to update the UI.
// app/counter/page.client.tsx
import { render } from 'melina/client';
function Counter({ count, onIncrement }: { count: number; onIncrement: () => void }) {
return (
<div>
<span>Count: {count}</span>
<button onClick={onIncrement}>+1</button>
</div>
);
}
export default function mount() {
const root = document.getElementById('counter-root');
if (!root) return;
let count = 0;
const update = () => {
render(<Counter count={count} onIncrement={() => { count++; update(); }} />, root);
};
update();
// Cleanup — called when navigating away
return () => render(null, root);
}
| File | Lifecycle | Use Case |
|---|---|---|
| page.client.tsx | Mounts on page load, unmounts on navigate away | Page-specific interactions |
| layout.client.tsx | Mounts once, survives across navigations | Global widgets, persistent state |
Both must export default function mount():
render(vnode, container) to mount/update contentThree diffing strategies:
import { setReconciler } from 'melina/client';
setReconciler('auto'); // Default — inspects children for keys
setReconciler('keyed'); // Best for dynamic lists
setReconciler('sequential'); // Best for static layouts
| Use Case | Strategy |
|---|---|
| Dynamic lists with reorder | keyed |
| Static forms, fixed layouts | sequential |
| Large lists (1000+) | keyed |
| Not sure | auto (default) |
route.ts)Export named HTTP method handlers:
// app/api/messages/route.ts
export async function GET(req: Request) {
return Response.json([{ id: 1, text: 'Hello' }]);
}
export async function POST(req: Request) {
const body = await req.json();
return Response.json({ ok: true });
}
Return an AsyncGenerator for Server-Sent Events:
// app/api/stream/route.ts
export async function* GET(req: Request) {
for (let i = 0; i < 10; i++) {
yield `data: ${JSON.stringify({ count: i })}\n\n`;
await new Promise(r => setTimeout(r, 1000));
}
}
middleware.ts)Middleware functions run before page rendering, root→leaf:
// app/middleware.ts
export default async function middleware(req: Request) {
const token = req.headers.get('authorization');
if (!token) {
return new Response('Unauthorized', { status: 401 });
}
// Return nothing to continue to the page
}
error.tsx)Catches render errors and displays them with full layout chrome:
// app/error.tsx
export default function ErrorPage({ error }: { error: { message: string; stack?: string } }) {
return (
<div style={{ padding: '40px', color: '#ef4444' }}>
<h1>Something went wrong</h1>
<pre>{error.message}</pre>
</div>
);
}
Pre-render pages at startup, serve from memory:
// app/pricing/page.tsx
export const ssg = true;
export default function PricingPage() {
return <main><h1>Pricing</h1></main>;
}
With TTL (time-to-live):
export const ssg = { revalidate: 60 }; // re-render after 60 seconds
SSG API:
import { getPrerendered, setPrerendered, clearSSGCache } from 'melina/server';
// Check if a page is cached
const html = getPrerendered('/pricing');
// Clear cache (e.g., after content update)
clearSSGCache('/pricing');
Create app/globals.css:
@import "tailwindcss";
@theme {
--color-background: #0a0a0f;
--color-surface: #111118;
--color-foreground: #e4e4e7;
--color-accent: #6366f1;
}
Melina auto-discovers globals.css, global.css, or app.css.
Place page.css or style.css alongside a page — auto-injected for that route only.
You don't need Tailwind — any CSS works. Just use standard stylesheets.
| Import Path | Module | Use Case |
|---|---|---|
| melina | src/web.ts | Server: start, serve, createAppRouter |
| melina/web | src/web.ts | Same as above |
| melina/server | src/server/index.ts | Server: renderToString, Head, SSG, build helpers |
| melina/server/ssr | src/server/ssr.ts | Server: renderToString only |
| melina/client | src/client/index.ts | Client: render, createElement, setReconciler, navigate, Link |
| melina/client/render | src/client/render.ts | Client: same as above (direct) |
| melina/client/types | src/client/types.ts | Types: VNode, Props, Component |
| melina/client/reconcilers | src/client/reconcilers/index.ts | Reconciler strategies |
| melina/client/jsx-runtime | src/client/jsx-runtime.ts | JSX transform (auto, don't import manually) |
| melina/client/jsx-dom | src/client/jsx-dom.ts | Real DOM JSX factory |
src/client/) are bundled for the browser — no server coderenderToString) lives at src/server/ssr.ts — never in client bundlesmelina/server or melina/webmelina/clientstart(options) — Quick startimport { start } from 'melina';
await start({
appDir: './app', // default: './app'
port: 3000, // default: 3000 or BUN_PORT env
defaultTitle: 'My App',
// hotReload: true, // opt-in in dev, off by default
});
serve(handler, options) + createAppRouter(options) — Advancedimport { serve, createAppRouter } from 'melina';
const router = createAppRouter({
appDir: './app',
defaultTitle: 'My App',
globalCss: './app/globals.css',
});
serve(router, { port: 3000 });
import { buildScript, buildStyle, buildAsset } from 'melina/server';
const scriptUrl = await buildScript('./src/app.ts'); // JS/TS → hashed URL
const cssUrl = await buildStyle('./src/style.css'); // CSS → processed URL
const imgUrl = await buildAsset(Bun.file('./icon.png')); // Static → hashed URL
For dynamic lists, use data-* attributes with delegated event handlers:
// In page.client.tsx
document.addEventListener('click', (e) => {
const btn = (e.target as HTMLElement).closest('[data-item-id]');
if (btn) {
const id = (btn as HTMLElement).dataset.itemId;
selectItem(id);
}
});
// In component
function Item({ item }: { item: { id: string; name: string } }) {
return <button data-item-id={item.id}>{item.name}</button>;
}
// layout.client.tsx — survives across navigations
import { render } from 'melina/client';
export default function mount() {
const root = document.getElementById('widget-root');
if (!root) return;
let isOpen = false;
const update = () => render(
<div>
{isOpen && <div className="widget-panel">Widget content</div>}
<button onClick={() => { isOpen = !isOpen; update(); }}>
{isOpen ? '✕' : '?'}
</button>
</div>,
root
);
update();
return () => render(null, root);
}
import { navigate } from 'melina/client';
// Programmatic navigation with View Transitions
navigate('/dashboard');
async Server ComponentsMelina's renderToString does NOT support async server components.
export default async function Page() { ... }export default function Page() { ... }If you need async data, fetch it before the component renders and pass it as props.
mount() exportClient scripts MUST export default function mount(). Without this, no interactivity.
Melina auto-maps app/globals.css → /globals.css. Don't use custom paths in <link> tags.
On Windows, stale Bun processes may hold ports:
netstat -ano | findstr ":3000"
taskkill /F /PID <PID>
After switching package versions or linking locally:
Remove-Item -Recurse -Force node_modules, bun.lock; bun install
tools
Use when work should span one or more detached tasks but still behave like one job with a single owner context. TaskFlow is the durable flow substrate under authoring layers like Lobster, ACPX, plugins, or plain code. Keep conditional logic in the caller; use TaskFlow for flow identity, child-task linkage, waiting state, revision-checked mutations, and user-facing emergence.
tools
# Lobster Lobster executes multi-step workflows with approval checkpoints. Use it when: - User wants a repeatable automation (triage, monitor, sync) - Actions need human approval before executing (send, post, delete) - Multiple tool calls should run as one deterministic operation ## When to use Lobster | User intent | Use Lobster? | | ------------------------------------------------------ | --------------------------
tools
# Lobster Lobster executes multi-step workflows with approval checkpoints. Use it when: - User wants a repeatable automation (triage, monitor, sync) - Actions need human approval before executing (send, post, delete) - Multiple tool calls should run as one deterministic operation ## When to use Lobster | User intent | Use Lobster? | | ------------------------------------------------------ | --------------------------
tools
A CLI tool for making authenticated requests to the X (Twitter) API. Use this skill when you need to post tweets, reply, quote, search, read posts, manage followers, send DMs, upload media, or interact with any X API v2 endpoint.