一键导入
melina
Bun-native web framework with file routing. Use this for building server-rendered pages with client mount scripts for interactivity.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Bun-native web framework with file routing. Use this for building server-rendered pages with client mount scripts for interactivity.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
CLI tool for patching files and resolving file includes - useful when working with LLMs
How to maintain project task docs and handle "whats next" requests. Read this when the user asks whats next or at conversation start.
Git workflow conventions — commit hygiene, squashing, force-push, and interactive rebase patterns.
Lightweight Bun process manager. Use bgrun to start, stop, restart, watch, and monitor background processes — with built-in web dashboard, TOML config, Docker support, and programmatic API.
Core behavioral rule: say 'I DON'T KNOW' when you don't actually know something. Never fabricate confident answers from incomplete information.
Install, uninstall, and list agent skills. Handles NTFS hardlinks for auto-sync with source repos.
| name | melina |
| description | Bun-native web framework with file routing. Use this for building server-rendered pages with client mount scripts for interactivity. |
Melina.js 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.3.1
Use Melina when the user wants:
Melina 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 melina
Create server.ts:
import { start } from 'melina';
import path from 'path';
const appDir = path.join(import.meta.dir, 'app');
await start({
port: parseInt(process.env.BUN_PORT || "3000"),
appDir,
defaultTitle: 'My App',
});
Create tsconfig.json:
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "melina/client",
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"noEmit": true,
"skipLibCheck": true
},
"include": ["app/**/*", "server.ts"]
}
Critical:
"jsxImportSource": "melina/client"is required. This tells TypeScript and Bun to use Melina's VDOM runtime for JSX instead of React.
When building examples inside the melina.js repo, import from local source:
// server.ts (inside examples/)
import { start } from '../../src/web';
And use paths in tsconfig.json:
{
"compilerOptions": {
"paths": {
"melina/client/*": ["../../src/client/*"],
"melina/*": ["../../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',
});
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