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 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
+ [![npm version](https://img.shields.io/npm/v/@ments/web)](https://www.npmjs.com/package/@ments/web)
6
+ [![Bun](https://img.shields.io/badge/runtime-Bun-f9f1e1)](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)