melina 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/GUIDE.md +538 -0
- package/README.md +132 -0
- package/bin/melina +95 -0
- package/docs/ARCHITECTURE.md +856 -0
- package/docs/islands-hydration.png +0 -0
- package/package.json +68 -0
- package/src/IslandOrchestrator.tsx +259 -0
- package/src/Link.tsx +65 -0
- package/src/clientRegistry.ts +81 -0
- package/src/createIsland.tsx +121 -0
- package/src/island.ts +69 -0
- package/src/islands.ts +93 -0
- package/src/loader.ts +226 -0
- package/src/mcp.ts +716 -0
- package/src/plugin.ts +236 -0
- package/src/preload.ts +124 -0
- package/src/preprocess.ts +204 -0
- package/src/router.ts +192 -0
- package/src/runtime/hangar.ts +399 -0
- package/src/runtime.ts +223 -0
- package/src/transform.ts +168 -0
- package/src/web.ts +1324 -0
package/GUIDE.md
ADDED
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
# Melina.js Developer Guide
|
|
2
|
+
|
|
3
|
+
**A lightweight, islands-architecture framework for Bun**
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
1. [Introduction](#introduction)
|
|
8
|
+
2. [Core Concepts](#core-concepts)
|
|
9
|
+
3. [Quick Start](#quick-start)
|
|
10
|
+
4. [Server Components](#server-components)
|
|
11
|
+
5. [Client Components (Islands)](#client-components-islands)
|
|
12
|
+
6. [The Router](#the-router)
|
|
13
|
+
7. [Layouts](#layouts)
|
|
14
|
+
8. [State Persistence](#state-persistence)
|
|
15
|
+
9. [Cross-Island Communication](#cross-island-communication)
|
|
16
|
+
10. [Best Practices & Gotchas](#best-practices--gotchas)
|
|
17
|
+
11. [API Reference](#api-reference)
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Introduction
|
|
22
|
+
|
|
23
|
+
Melina.js is a Next.js-compatible framework with a simpler architecture, built specifically for Bun. It uses the **Islands Architecture** where:
|
|
24
|
+
|
|
25
|
+
- **Server Components**: Rendered on the server, sent as HTML
|
|
26
|
+
- **Client Components (Islands)**: Interactive React components that hydrate on the client
|
|
27
|
+
|
|
28
|
+
### Key Features
|
|
29
|
+
|
|
30
|
+
- ✅ File-based routing (Next.js App Router style)
|
|
31
|
+
- ✅ Nested layouts with automatic composition
|
|
32
|
+
- ✅ Islands architecture for optimal performance
|
|
33
|
+
- ✅ SPA-like navigation with partial page swaps
|
|
34
|
+
- ✅ CSS animations that don't reset on navigation
|
|
35
|
+
- ✅ Streaming HTML responses
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Core Concepts
|
|
40
|
+
|
|
41
|
+
### The Two Graphs
|
|
42
|
+
|
|
43
|
+
Melina.js separates your app into two distinct "graphs":
|
|
44
|
+
|
|
45
|
+
1. **Server Graph** - Layouts and pages that render on the server
|
|
46
|
+
2. **Client Graph** - Interactive components (islands) that run in the browser
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
50
|
+
│ SERVER (Bun) │
|
|
51
|
+
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
|
52
|
+
│ │ layout.tsx │ → │ page.tsx │ → │ HTML │ ──────┼──→
|
|
53
|
+
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
|
54
|
+
└─────────────────────────────────────────────────────────────┘
|
|
55
|
+
│
|
|
56
|
+
↓
|
|
57
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
58
|
+
│ BROWSER │
|
|
59
|
+
│ ┌────────────────────────────────────────────────────────┐│
|
|
60
|
+
│ │ Static HTML + Islands (hydrated React components) ││
|
|
61
|
+
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ││
|
|
62
|
+
│ │ │ Counter │ │ SearchBar│ │ JobTracker│ ││
|
|
63
|
+
│ │ │ (island) │ │ (island) │ │ (island) │ ││
|
|
64
|
+
│ │ └──────────┘ └──────────┘ └──────────┘ ││
|
|
65
|
+
│ └────────────────────────────────────────────────────────┘│
|
|
66
|
+
└─────────────────────────────────────────────────────────────┘
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Partial Page Swaps
|
|
70
|
+
|
|
71
|
+
When you navigate between pages, Melina.js doesn't replace the entire `<body>`. Instead:
|
|
72
|
+
|
|
73
|
+
1. Fetches the new page HTML
|
|
74
|
+
2. Finds `#melina-page-content` in both current and new page
|
|
75
|
+
3. Only replaces that container's innerHTML
|
|
76
|
+
4. **Header/footer remain untouched** (preserving state and animations!)
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Quick Start
|
|
81
|
+
|
|
82
|
+
### Installation
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
bun add @anthropic/melina.js
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Create Your App Structure
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
my-app/
|
|
92
|
+
├── app/
|
|
93
|
+
│ ├── layout.tsx # Root layout
|
|
94
|
+
│ ├── page.tsx # Home page (/)
|
|
95
|
+
│ ├── about/
|
|
96
|
+
│ │ └── page.tsx # About page (/about)
|
|
97
|
+
│ ├── components/
|
|
98
|
+
│ │ ├── Counter.tsx # 'use client' component
|
|
99
|
+
│ │ └── SearchBar.tsx # 'use client' component
|
|
100
|
+
│ └── globals.css # Global styles
|
|
101
|
+
└── package.json
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Start the Dev Server
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
bunx melina start
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Server Components
|
|
113
|
+
|
|
114
|
+
Server Components are the default. They render on the server and send HTML to the client.
|
|
115
|
+
|
|
116
|
+
```tsx
|
|
117
|
+
// app/page.tsx - Server Component (default)
|
|
118
|
+
export default function HomePage() {
|
|
119
|
+
const data = await fetchFromDatabase(); // Can use async!
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<div>
|
|
123
|
+
<h1>Welcome</h1>
|
|
124
|
+
<p>Data: {data}</p>
|
|
125
|
+
</div>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### What Server Components CAN do:
|
|
131
|
+
- ✅ Async/await directly in the component
|
|
132
|
+
- ✅ Access databases, file systems, environment variables
|
|
133
|
+
- ✅ Import other Server Components
|
|
134
|
+
- ✅ Import Client Components (they'll become islands)
|
|
135
|
+
|
|
136
|
+
### What Server Components CANNOT do:
|
|
137
|
+
- ❌ Use `useState`, `useEffect`, or other hooks
|
|
138
|
+
- ❌ Use browser APIs (`window`, `localStorage`)
|
|
139
|
+
- ❌ Handle click events
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Client Components (Islands)
|
|
144
|
+
|
|
145
|
+
Add `'use client'` at the top of your file to make it interactive:
|
|
146
|
+
|
|
147
|
+
```tsx
|
|
148
|
+
// app/components/Counter.tsx
|
|
149
|
+
'use client';
|
|
150
|
+
|
|
151
|
+
import { useState } from 'react';
|
|
152
|
+
|
|
153
|
+
export function Counter({ initialCount = 0 }) {
|
|
154
|
+
const [count, setCount] = useState(initialCount);
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<button onClick={() => setCount(c => c + 1)}>
|
|
158
|
+
Count: {count}
|
|
159
|
+
</button>
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Using Islands in Server Components
|
|
165
|
+
|
|
166
|
+
Just import and use them:
|
|
167
|
+
|
|
168
|
+
```tsx
|
|
169
|
+
// app/page.tsx - Server Component
|
|
170
|
+
import { Counter } from './components/Counter';
|
|
171
|
+
|
|
172
|
+
export default function HomePage() {
|
|
173
|
+
return (
|
|
174
|
+
<div>
|
|
175
|
+
<h1>My Page</h1>
|
|
176
|
+
<Counter initialCount={10} /> {/* Island! */}
|
|
177
|
+
</div>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
The framework automatically:
|
|
183
|
+
1. Renders the component on the server (for SEO/initial paint)
|
|
184
|
+
2. Adds `data-melina-island` markers
|
|
185
|
+
3. Hydrates on the client with React
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## The Router
|
|
190
|
+
|
|
191
|
+
### File-Based Routes
|
|
192
|
+
|
|
193
|
+
```
|
|
194
|
+
app/page.tsx → /
|
|
195
|
+
app/about/page.tsx → /about
|
|
196
|
+
app/blog/[id]/page.tsx → /blog/:id (dynamic)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### The `<Link>` Component
|
|
200
|
+
|
|
201
|
+
Use `<Link>` for SPA-style navigation:
|
|
202
|
+
|
|
203
|
+
```tsx
|
|
204
|
+
import { Link } from '@anthropic/melina.js';
|
|
205
|
+
|
|
206
|
+
export default function Nav() {
|
|
207
|
+
return (
|
|
208
|
+
<nav>
|
|
209
|
+
<Link href="/">Home</Link>
|
|
210
|
+
<Link href="/about">About</Link>
|
|
211
|
+
</nav>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Navigation Lifecycle
|
|
217
|
+
|
|
218
|
+
When a user clicks a link:
|
|
219
|
+
|
|
220
|
+
1. **Fetch**: Router fetches the new page HTML
|
|
221
|
+
2. **Parse**: Parses it into a DOM tree
|
|
222
|
+
3. **Swap**: Only replaces `#melina-page-content`
|
|
223
|
+
4. **Hydrate**: Finds new islands and hydrates them
|
|
224
|
+
|
|
225
|
+
**Elements outside `#melina-page-content` persist automatically!**
|
|
226
|
+
|
|
227
|
+
### ⚡ What Happens to Islands During Navigation
|
|
228
|
+
|
|
229
|
+
Each island is its own **independent React root**. Here's what happens:
|
|
230
|
+
|
|
231
|
+
| Island Location | On Navigation | State |
|
|
232
|
+
|-----------------|---------------|-------|
|
|
233
|
+
| In Layout (header/footer) | **Persists** - DOM untouched | ✅ Preserved |
|
|
234
|
+
| In Page | **Destroyed** - innerHTML replaced | ❌ Lost |
|
|
235
|
+
| Same island on new page | New React root created | Fresh state |
|
|
236
|
+
|
|
237
|
+
**Example:**
|
|
238
|
+
```
|
|
239
|
+
Navigate from /home to /about:
|
|
240
|
+
|
|
241
|
+
┌─ Layout ────────────────────────────────────────┐
|
|
242
|
+
│ <SearchBar /> ← React Root #1 (STAYS) │
|
|
243
|
+
│ state preserved! ✅ │
|
|
244
|
+
├─────────────────────────────────────────────────┤
|
|
245
|
+
│ <main id="melina-page-content"> │
|
|
246
|
+
│ /home: <Counter /> ← Root #2 (DESTROYED) ❌ │
|
|
247
|
+
│ /about: <ContactForm /> ← Root #3 (NEW) 🆕 │
|
|
248
|
+
│ </main> │
|
|
249
|
+
├─────────────────────────────────────────────────┤
|
|
250
|
+
│ <JobTracker /> ← React Root #4 (STAYS) │
|
|
251
|
+
│ state preserved! ✅ │
|
|
252
|
+
└─────────────────────────────────────────────────┘
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
**Key insight:** Even if both pages use `<Counter />`, they are **different React roots**. State doesn't transfer.
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## Layouts
|
|
260
|
+
|
|
261
|
+
### Root Layout
|
|
262
|
+
|
|
263
|
+
Every app needs a root `layout.tsx`:
|
|
264
|
+
|
|
265
|
+
```tsx
|
|
266
|
+
// app/layout.tsx
|
|
267
|
+
import { SearchBar } from './components/SearchBar';
|
|
268
|
+
import { JobTracker } from './components/JobTracker';
|
|
269
|
+
|
|
270
|
+
export default function RootLayout({ children }) {
|
|
271
|
+
return (
|
|
272
|
+
<html>
|
|
273
|
+
<body>
|
|
274
|
+
<header>
|
|
275
|
+
<nav>...</nav>
|
|
276
|
+
<SearchBar /> {/* Persists across navigation! */}
|
|
277
|
+
</header>
|
|
278
|
+
|
|
279
|
+
{/* Only this part swaps on navigation */}
|
|
280
|
+
<main id="melina-page-content">
|
|
281
|
+
{children}
|
|
282
|
+
</main>
|
|
283
|
+
|
|
284
|
+
<footer>...</footer>
|
|
285
|
+
<JobTracker /> {/* Persists too! */}
|
|
286
|
+
</body>
|
|
287
|
+
</html>
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### Nested Layouts
|
|
293
|
+
|
|
294
|
+
```
|
|
295
|
+
app/
|
|
296
|
+
├── layout.tsx # Root layout
|
|
297
|
+
└── dashboard/
|
|
298
|
+
├── layout.tsx # Dashboard layout (nested)
|
|
299
|
+
└── page.tsx # Dashboard page
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
## State Persistence
|
|
305
|
+
|
|
306
|
+
### Why Islands Persist
|
|
307
|
+
|
|
308
|
+
Islands in the layout (outside `#melina-page-content`) are never destroyed during navigation:
|
|
309
|
+
|
|
310
|
+
```tsx
|
|
311
|
+
// This SearchBar's React state survives navigation!
|
|
312
|
+
<header>
|
|
313
|
+
<SearchBar />
|
|
314
|
+
</header>
|
|
315
|
+
|
|
316
|
+
<main id="melina-page-content">
|
|
317
|
+
{children} {/* Only this changes */}
|
|
318
|
+
</main>
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### ⚠️ The Props Desynchronization Trade-off
|
|
322
|
+
|
|
323
|
+
**This is important!** When navigating:
|
|
324
|
+
|
|
325
|
+
1. Server renders new page with new props
|
|
326
|
+
2. Router keeps old layout (to preserve state)
|
|
327
|
+
3. **Old islands keep their OLD props!**
|
|
328
|
+
|
|
329
|
+
**Example:**
|
|
330
|
+
```tsx
|
|
331
|
+
// layout.tsx
|
|
332
|
+
<Header activeTab={pathname} /> // ❌ Props won't update!
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
If you're on `/home` and navigate to `/about`:
|
|
336
|
+
- Server sends `activeTab="about"`
|
|
337
|
+
- Router throws that away, keeps old Header
|
|
338
|
+
- Header still shows `activeTab="home"` 😱
|
|
339
|
+
|
|
340
|
+
### Solution: Use URL State
|
|
341
|
+
|
|
342
|
+
Persistent islands should read from URL, not props:
|
|
343
|
+
|
|
344
|
+
```tsx
|
|
345
|
+
// ✅ CORRECT - Read from URL
|
|
346
|
+
'use client';
|
|
347
|
+
|
|
348
|
+
export function Header() {
|
|
349
|
+
const [activeTab, setActiveTab] = useState('/');
|
|
350
|
+
|
|
351
|
+
useEffect(() => {
|
|
352
|
+
// Read current path
|
|
353
|
+
setActiveTab(window.location.pathname);
|
|
354
|
+
|
|
355
|
+
// Update on navigation
|
|
356
|
+
const handleNav = () => setActiveTab(window.location.pathname);
|
|
357
|
+
window.addEventListener('melina:navigated', handleNav);
|
|
358
|
+
return () => window.removeEventListener('melina:navigated', handleNav);
|
|
359
|
+
}, []);
|
|
360
|
+
|
|
361
|
+
return (
|
|
362
|
+
<nav>
|
|
363
|
+
<Link href="/" className={activeTab === '/' ? 'active' : ''}>Home</Link>
|
|
364
|
+
<Link href="/about" className={activeTab === '/about' ? 'active' : ''}>About</Link>
|
|
365
|
+
</nav>
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
---
|
|
371
|
+
|
|
372
|
+
## Cross-Island Communication
|
|
373
|
+
|
|
374
|
+
Islands are independent React roots. They can't share state directly.
|
|
375
|
+
|
|
376
|
+
### Option 1: localStorage + Events
|
|
377
|
+
|
|
378
|
+
```tsx
|
|
379
|
+
// Island A: Emit event
|
|
380
|
+
function submitJob(name) {
|
|
381
|
+
const job = { id: Date.now(), name };
|
|
382
|
+
localStorage.setItem('lastJob', JSON.stringify(job));
|
|
383
|
+
window.dispatchEvent(new CustomEvent('job:created', { detail: job }));
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Island B: Listen for events
|
|
387
|
+
useEffect(() => {
|
|
388
|
+
const handler = (e) => setJobs(prev => [...prev, e.detail]);
|
|
389
|
+
window.addEventListener('job:created', handler);
|
|
390
|
+
return () => window.removeEventListener('job:created', handler);
|
|
391
|
+
}, []);
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### Option 2: Event Bus
|
|
395
|
+
|
|
396
|
+
```tsx
|
|
397
|
+
// eventBus.ts
|
|
398
|
+
export const eventBus = {
|
|
399
|
+
emit(event: string, data: any) {
|
|
400
|
+
window.dispatchEvent(new CustomEvent(`app:${event}`, { detail: data }));
|
|
401
|
+
},
|
|
402
|
+
|
|
403
|
+
on(event: string, callback: (data: any) => void) {
|
|
404
|
+
const handler = (e: CustomEvent) => callback(e.detail);
|
|
405
|
+
window.addEventListener(`app:${event}`, handler);
|
|
406
|
+
return () => window.removeEventListener(`app:${event}`, handler);
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
### Option 3: URL State
|
|
412
|
+
|
|
413
|
+
For state that should survive refresh:
|
|
414
|
+
|
|
415
|
+
```tsx
|
|
416
|
+
// Read from URL
|
|
417
|
+
const searchParams = new URLSearchParams(window.location.search);
|
|
418
|
+
const filter = searchParams.get('filter');
|
|
419
|
+
|
|
420
|
+
// Update URL
|
|
421
|
+
function setFilter(value) {
|
|
422
|
+
const url = new URL(window.location.href);
|
|
423
|
+
url.searchParams.set('filter', value);
|
|
424
|
+
window.history.replaceState({}, '', url);
|
|
425
|
+
}
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
---
|
|
429
|
+
|
|
430
|
+
## Best Practices & Gotchas
|
|
431
|
+
|
|
432
|
+
### ✅ Do's
|
|
433
|
+
|
|
434
|
+
1. **Keep layouts lean** - Only put truly persistent elements there
|
|
435
|
+
2. **Use URL state for persistent islands** - Don't rely on server props
|
|
436
|
+
3. **Use `#melina-page-content`** - The router needs this container
|
|
437
|
+
4. **Listen for `melina:navigated`** - Keep persistent islands synced
|
|
438
|
+
|
|
439
|
+
### ❌ Don'ts
|
|
440
|
+
|
|
441
|
+
1. **Don't pass dynamic props to persistent islands** - They won't update
|
|
442
|
+
2. **Don't use global CSS transitions on body** - They'll restart on swap
|
|
443
|
+
3. **Don't forget `display: contents`** - If island wrapper breaks layout
|
|
444
|
+
|
|
445
|
+
### Common Patterns
|
|
446
|
+
|
|
447
|
+
```tsx
|
|
448
|
+
// ✅ Good: Persistent island reads URL
|
|
449
|
+
function NavTabs() {
|
|
450
|
+
const [path, setPath] = useState('/');
|
|
451
|
+
useEffect(() => {
|
|
452
|
+
setPath(window.location.pathname);
|
|
453
|
+
window.addEventListener('melina:navigated', () => {
|
|
454
|
+
setPath(window.location.pathname);
|
|
455
|
+
});
|
|
456
|
+
}, []);
|
|
457
|
+
return <nav>...</nav>;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// ✅ Good: Cross-page data via localStorage
|
|
461
|
+
function JobTracker() {
|
|
462
|
+
const [jobs, setJobs] = useState(() => {
|
|
463
|
+
return JSON.parse(localStorage.getItem('jobs') || '[]');
|
|
464
|
+
});
|
|
465
|
+
// ...
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// ❌ Bad: Relying on props in persistent island
|
|
469
|
+
function Header({ activeTab }) { // Won't update on navigation!
|
|
470
|
+
return <nav className={activeTab}>...</nav>;
|
|
471
|
+
}
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
---
|
|
475
|
+
|
|
476
|
+
## API Reference
|
|
477
|
+
|
|
478
|
+
### CLI Commands
|
|
479
|
+
|
|
480
|
+
```bash
|
|
481
|
+
melina init <project-name> # Create new project
|
|
482
|
+
melina start # Start dev server
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
### Components
|
|
486
|
+
|
|
487
|
+
```tsx
|
|
488
|
+
import { Link } from '@anthropic/melina.js';
|
|
489
|
+
|
|
490
|
+
<Link href="/path">Text</Link>
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
### Events
|
|
494
|
+
|
|
495
|
+
```js
|
|
496
|
+
// Fired after SPA navigation completes
|
|
497
|
+
window.addEventListener('melina:navigated', () => {
|
|
498
|
+
console.log('Navigation complete!');
|
|
499
|
+
});
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
### Server Functions
|
|
503
|
+
|
|
504
|
+
```tsx
|
|
505
|
+
import { serve, createAppRouter } from '@anthropic/melina.js';
|
|
506
|
+
|
|
507
|
+
serve(createAppRouter({
|
|
508
|
+
appDir: './app',
|
|
509
|
+
defaultTitle: 'My App',
|
|
510
|
+
}), { port: 3000 });
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
---
|
|
514
|
+
|
|
515
|
+
## Troubleshooting
|
|
516
|
+
|
|
517
|
+
### "My island doesn't update on navigation"
|
|
518
|
+
|
|
519
|
+
You're probably relying on server props. See [Props Desynchronization](#️-the-props-desynchronization-trade-off).
|
|
520
|
+
|
|
521
|
+
### "CSS animations restart on navigation"
|
|
522
|
+
|
|
523
|
+
Make sure your animated elements are **outside** `#melina-page-content`.
|
|
524
|
+
|
|
525
|
+
### "Islands don't communicate"
|
|
526
|
+
|
|
527
|
+
Islands are separate React roots. Use localStorage/events. See [Cross-Island Communication](#cross-island-communication).
|
|
528
|
+
|
|
529
|
+
---
|
|
530
|
+
|
|
531
|
+
## Version History
|
|
532
|
+
|
|
533
|
+
- **v0.2.0** - Partial page swap architecture, persistent islands
|
|
534
|
+
- **v0.1.0** - Initial release with file-based routing
|
|
535
|
+
|
|
536
|
+
---
|
|
537
|
+
|
|
538
|
+
Built with ❤️ using Bun
|
package/README.md
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Melina.js 🦊
|
|
2
|
+
|
|
3
|
+
**A lightweight, islands-architecture web framework for Bun**
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@ments/web)
|
|
6
|
+
[](https://bun.sh)
|
|
7
|
+
|
|
8
|
+
Melina.js is a Next.js-compatible framework with a radically simpler architecture. Built specifically for Bun, it eliminates the need for external bundlers by leveraging Bun's native build APIs.
|
|
9
|
+
|
|
10
|
+
## ✨ Features
|
|
11
|
+
|
|
12
|
+
- 🏝️ **Islands Architecture** — Only hydrate what needs to be interactive
|
|
13
|
+
- 📁 **File-based Routing** — Next.js App Router style (`app/page.tsx` → `/`)
|
|
14
|
+
- ⚡ **In-Memory Builds** — No `dist/` folder, assets built and served from RAM
|
|
15
|
+
- 🔄 **Partial Page Swaps** — SPA-like navigation without the SPA complexity
|
|
16
|
+
- 🎨 **Tailwind CSS v4** — Built-in support for CSS-first configuration
|
|
17
|
+
- 🌐 **Import Maps** — Browser-native module resolution, no vendor bundles
|
|
18
|
+
|
|
19
|
+
## 🚀 Quick Start
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# Install
|
|
23
|
+
bun add @ments/web
|
|
24
|
+
|
|
25
|
+
# Create app structure
|
|
26
|
+
mkdir -p app/components
|
|
27
|
+
|
|
28
|
+
# Create a page
|
|
29
|
+
cat > app/page.tsx << 'EOF'
|
|
30
|
+
export default function Home() {
|
|
31
|
+
return <h1>Hello Melina! 🦊</h1>;
|
|
32
|
+
}
|
|
33
|
+
EOF
|
|
34
|
+
|
|
35
|
+
# Create layout
|
|
36
|
+
cat > app/layout.tsx << 'EOF'
|
|
37
|
+
export default function Layout({ children }) {
|
|
38
|
+
return (
|
|
39
|
+
<html>
|
|
40
|
+
<body>
|
|
41
|
+
<main id="melina-page-content">{children}</main>
|
|
42
|
+
</body>
|
|
43
|
+
</html>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
EOF
|
|
47
|
+
|
|
48
|
+
# Start dev server
|
|
49
|
+
bunx melina start
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Open http://localhost:3000 🎉
|
|
53
|
+
|
|
54
|
+
## 🏝️ Creating Islands (Client Components)
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
// app/components/Counter.tsx
|
|
58
|
+
'use client';
|
|
59
|
+
|
|
60
|
+
import { useState } from 'react';
|
|
61
|
+
import { island } from '@ments/web/island';
|
|
62
|
+
|
|
63
|
+
function CounterImpl({ initialCount = 0 }) {
|
|
64
|
+
const [count, setCount] = useState(initialCount);
|
|
65
|
+
return (
|
|
66
|
+
<button onClick={() => setCount(c => c + 1)}>
|
|
67
|
+
Count: {count}
|
|
68
|
+
</button>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const Counter = island(CounterImpl, 'Counter');
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Use in pages:
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
// app/page.tsx
|
|
79
|
+
import { Counter } from './components/Counter';
|
|
80
|
+
|
|
81
|
+
export default function Home() {
|
|
82
|
+
return (
|
|
83
|
+
<div>
|
|
84
|
+
<h1>My App</h1>
|
|
85
|
+
<Counter initialCount={10} />
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## 📖 Documentation
|
|
92
|
+
|
|
93
|
+
- **[Developer Guide](./GUIDE.md)** — Core concepts, best practices, API reference
|
|
94
|
+
- **[Architecture Deep Dive](./docs/ARCHITECTURE.md)** — Technical internals
|
|
95
|
+
|
|
96
|
+
## 🔧 CLI
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
melina init <name> # Create new project
|
|
100
|
+
melina start # Start dev server
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## 📦 Project Structure
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
my-app/
|
|
107
|
+
├── app/
|
|
108
|
+
│ ├── layout.tsx # Root layout
|
|
109
|
+
│ ├── page.tsx # Home page (/)
|
|
110
|
+
│ ├── about/
|
|
111
|
+
│ │ └── page.tsx # /about
|
|
112
|
+
│ ├── api/
|
|
113
|
+
│ │ └── hello/
|
|
114
|
+
│ │ └── route.ts # API route
|
|
115
|
+
│ ├── components/
|
|
116
|
+
│ │ └── Counter.tsx # 'use client' component
|
|
117
|
+
│ └── globals.css # Global styles
|
|
118
|
+
└── package.json
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## 🤔 Why Melina?
|
|
122
|
+
|
|
123
|
+
| Traditional SPA | Melina.js |
|
|
124
|
+
|-----------------|-----------|
|
|
125
|
+
| Bundle everything | Islands hydrate selectively |
|
|
126
|
+
| Full page refresh or client routing | Partial page swaps |
|
|
127
|
+
| Complex Webpack/Vite config | Zero config, Bun-native |
|
|
128
|
+
| 100KB+ vendor chunks | Browser-native import maps |
|
|
129
|
+
|
|
130
|
+
## License
|
|
131
|
+
|
|
132
|
+
MIT © [Mements](https://github.com/mements)
|