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.
Binary file
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "melina",
3
+ "version": "1.0.0",
4
+ "description": "A lightweight, islands-architecture web framework for Bun with Next.js-style routing.",
5
+ "module": "./src/web.ts",
6
+ "main": "./src/web.ts",
7
+ "bin": {
8
+ "melina": "./bin/melina"
9
+ },
10
+ "files": [
11
+ "src",
12
+ "bin",
13
+ "docs",
14
+ "GUIDE.md",
15
+ "README.md"
16
+ ],
17
+ "exports": {
18
+ ".": "./src/web.ts",
19
+ "./web": "./src/web.ts",
20
+ "./Link": "./src/Link.tsx",
21
+ "./island": "./src/island.ts"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://github.com/mements/melina.js"
26
+ },
27
+ "author": {
28
+ "name": "Mements Team"
29
+ },
30
+ "bugs": "https://github.com/mements/melina.js/issues",
31
+ "homepage": "https://github.com/mements/melina.js#readme",
32
+ "license": "MIT",
33
+ "keywords": [
34
+ "bun",
35
+ "server",
36
+ "framework",
37
+ "typescript",
38
+ "web",
39
+ "ssr",
40
+ "islands",
41
+ "islands-architecture",
42
+ "hydration",
43
+ "nextjs",
44
+ "app-router",
45
+ "react",
46
+ "partial-hydration"
47
+ ],
48
+ "type": "module",
49
+ "private": false,
50
+ "sideEffects": false,
51
+ "devDependencies": {
52
+ "@types/bun": "latest",
53
+ "@types/react": "^19.2.10",
54
+ "@types/react-dom": "^19.2.3"
55
+ },
56
+ "dependencies": {
57
+ "@ments/utils": "^1.2.1",
58
+ "@modelcontextprotocol/sdk": "^1.11.0",
59
+ "@solana/kit": "^2.1.0",
60
+ "@tailwindcss/postcss": "^4.1.10",
61
+ "autoprefixer": "^10.4.21",
62
+ "postcss": "^8.5.6",
63
+ "react": "^19.1.1",
64
+ "react-dom": "^19.1.1",
65
+ "ts-dedent": "^2.2.0",
66
+ "zod": "^3.24.4"
67
+ }
68
+ }
@@ -0,0 +1,259 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * IslandOrchestrator - Single React Root + Portals Architecture
5
+ *
6
+ * This component manages ALL islands in the application from a single React root.
7
+ * Each island is rendered as a React Portal into its placeholder in the DOM.
8
+ *
9
+ * Benefits:
10
+ * - Single React instance (no duplicate bundles)
11
+ * - Shared state context across all islands
12
+ * - Islands persist across navigation (portals just re-target)
13
+ * - View Transitions work naturally (same React tree)
14
+ */
15
+
16
+ import React, { useState, useEffect, useCallback, useSyncExternalStore } from 'react';
17
+ import { createPortal, flushSync } from 'react-dom';
18
+
19
+ // Types
20
+ interface IslandConfig {
21
+ name: string;
22
+ props: Record<string, any>;
23
+ target: HTMLElement;
24
+ instanceId: string;
25
+ }
26
+
27
+ interface LoadedIsland extends IslandConfig {
28
+ Component: React.ComponentType<any>;
29
+ }
30
+
31
+ // Global component cache - shared across all orchestrator instances
32
+ const componentCache: Record<string, React.ComponentType<any>> = {};
33
+
34
+ // Get island meta from the page
35
+ function getIslandMeta(): Record<string, string> {
36
+ const metaEl = document.getElementById('__MELINA_META__');
37
+ if (!metaEl) return {};
38
+ try {
39
+ return JSON.parse(metaEl.textContent || '{}');
40
+ } catch {
41
+ return {};
42
+ }
43
+ }
44
+
45
+ // Collect all island placeholders from the DOM
46
+ function collectIslandPlaceholders(): IslandConfig[] {
47
+ const elements = document.querySelectorAll('[data-island]');
48
+ const islands: IslandConfig[] = [];
49
+
50
+ elements.forEach((el, index) => {
51
+ const name = el.getAttribute('data-island');
52
+ if (!name) return;
53
+
54
+ const propsStr = (el.getAttribute('data-props') || '{}').replace(/&quot;/g, '"');
55
+ let props = {};
56
+ try {
57
+ props = JSON.parse(propsStr);
58
+ } catch (e) {
59
+ console.warn(`[Melina] Invalid props for island ${name}:`, e);
60
+ }
61
+
62
+ // Generate stable instance ID
63
+ const instanceId = el.getAttribute('data-instance') || `${name}-${index}`;
64
+
65
+ islands.push({
66
+ name,
67
+ props,
68
+ target: el as HTMLElement,
69
+ instanceId
70
+ });
71
+ });
72
+
73
+ return islands;
74
+ }
75
+
76
+ // Load a component module dynamically
77
+ async function loadComponent(name: string): Promise<React.ComponentType<any> | null> {
78
+ // Check cache first
79
+ if (componentCache[name]) {
80
+ return componentCache[name];
81
+ }
82
+
83
+ const meta = getIslandMeta();
84
+ const bundlePath = meta[name];
85
+
86
+ if (!bundlePath) {
87
+ console.warn(`[Melina] No bundle found for island: ${name}`);
88
+ return null;
89
+ }
90
+
91
+ try {
92
+ const module = await import(/* @vite-ignore */ bundlePath);
93
+ // Try named export first, then default
94
+ const Component = module[name] || module.default;
95
+
96
+ if (Component) {
97
+ componentCache[name] = Component;
98
+ console.log(`[Melina] Loaded component: ${name}`);
99
+ return Component;
100
+ } else {
101
+ console.warn(`[Melina] No export found for component: ${name}`);
102
+ return null;
103
+ }
104
+ } catch (e) {
105
+ console.error(`[Melina] Failed to load component ${name}:`, e);
106
+ return null;
107
+ }
108
+ }
109
+
110
+ // Island wrapper - renders the component in a portal
111
+ function IslandPortal({ island }: { island: LoadedIsland }) {
112
+ const { Component, props, target, name, instanceId } = island;
113
+
114
+ // Mark as hydrated
115
+ useEffect(() => {
116
+ target.setAttribute('data-hydrated', 'true');
117
+ return () => {
118
+ target.removeAttribute('data-hydrated');
119
+ };
120
+ }, [target]);
121
+
122
+ return createPortal(
123
+ <Component {...props} />,
124
+ target
125
+ );
126
+ }
127
+
128
+ // Navigation state store (external store pattern for React 18)
129
+ let currentPath = typeof window !== 'undefined' ? window.location.pathname : '/';
130
+ const pathListeners = new Set<() => void>();
131
+
132
+ function subscribePath(callback: () => void) {
133
+ pathListeners.add(callback);
134
+ return () => pathListeners.delete(callback);
135
+ }
136
+
137
+ function getPath() {
138
+ return currentPath;
139
+ }
140
+
141
+ function setPath(newPath: string) {
142
+ currentPath = newPath;
143
+ pathListeners.forEach(cb => cb());
144
+ }
145
+
146
+ // Listen for navigation events
147
+ if (typeof window !== 'undefined') {
148
+ window.addEventListener('melina:navigation-start', (e: any) => {
149
+ const to = e.detail?.to || window.location.pathname;
150
+ setPath(to);
151
+ });
152
+
153
+ window.addEventListener('popstate', () => {
154
+ setPath(window.location.pathname);
155
+ });
156
+ }
157
+
158
+ /**
159
+ * IslandOrchestrator Component
160
+ *
161
+ * Manages all islands from a single React root using portals.
162
+ */
163
+ export function IslandOrchestrator() {
164
+ const [islands, setIslands] = useState<LoadedIsland[]>([]);
165
+ const [isLoading, setIsLoading] = useState(true);
166
+
167
+ // Track current path for re-rendering on navigation
168
+ const path = useSyncExternalStore(subscribePath, getPath, () => '/');
169
+
170
+ // Scan and load islands
171
+ const scanAndLoadIslands = useCallback(async () => {
172
+ const placeholders = collectIslandPlaceholders();
173
+
174
+ // Load all components in parallel
175
+ const loaded: LoadedIsland[] = [];
176
+
177
+ await Promise.all(
178
+ placeholders.map(async (config) => {
179
+ const Component = await loadComponent(config.name);
180
+ if (Component) {
181
+ loaded.push({ ...config, Component });
182
+ }
183
+ })
184
+ );
185
+
186
+ // Use flushSync during navigation to ensure view transition captures correct state
187
+ if (document.startViewTransition) {
188
+ flushSync(() => setIslands(loaded));
189
+ } else {
190
+ setIslands(loaded);
191
+ }
192
+
193
+ setIsLoading(false);
194
+ console.log(`[Melina] Orchestrator: ${loaded.length} islands active`);
195
+ }, []);
196
+
197
+ // Initial scan
198
+ useEffect(() => {
199
+ scanAndLoadIslands();
200
+ }, [scanAndLoadIslands]);
201
+
202
+ // Re-scan on navigation
203
+ useEffect(() => {
204
+ const handleNavigated = () => {
205
+ // Small delay to let DOM update complete
206
+ requestAnimationFrame(() => {
207
+ scanAndLoadIslands();
208
+ });
209
+ };
210
+
211
+ window.addEventListener('melina:navigated', handleNavigated);
212
+ return () => window.removeEventListener('melina:navigated', handleNavigated);
213
+ }, [scanAndLoadIslands]);
214
+
215
+ // Also re-scan when path changes (for layout islands that conditionally render)
216
+ useEffect(() => {
217
+ scanAndLoadIslands();
218
+ }, [path, scanAndLoadIslands]);
219
+
220
+ // Render all islands as portals
221
+ return (
222
+ <>
223
+ {islands.map((island) => (
224
+ <IslandPortal
225
+ key={island.instanceId}
226
+ island={island}
227
+ />
228
+ ))}
229
+ </>
230
+ );
231
+ }
232
+
233
+ /**
234
+ * Initialize the Island Orchestrator
235
+ * Call this from the runtime script to bootstrap the single React root
236
+ */
237
+ export async function initializeOrchestrator() {
238
+ const React = await import('react');
239
+ const { createRoot } = await import('react-dom/client');
240
+
241
+ // Create container if it doesn't exist
242
+ let container = document.getElementById('melina-islands');
243
+ if (!container) {
244
+ container = document.createElement('div');
245
+ container.id = 'melina-islands';
246
+ container.style.display = 'contents'; // Won't affect layout
247
+ document.body.insertBefore(container, document.body.firstChild);
248
+ }
249
+
250
+ // Create single React root
251
+ const root = createRoot(container);
252
+ root.render(React.createElement(IslandOrchestrator));
253
+
254
+ console.log('[Melina] Island Orchestrator initialized (Single Root + Portals)');
255
+
256
+ return root;
257
+ }
258
+
259
+ export default IslandOrchestrator;
package/src/Link.tsx ADDED
@@ -0,0 +1,65 @@
1
+ import React from 'react';
2
+
3
+ export interface LinkProps {
4
+ href: string;
5
+ children: React.ReactNode;
6
+ className?: string;
7
+ style?: React.CSSProperties;
8
+ }
9
+
10
+ /**
11
+ * Link Component
12
+ *
13
+ * Uses the Hangar runtime's navigation function (window.melinaNavigate)
14
+ * for proper View Transitions and island state preservation.
15
+ */
16
+ export function Link({ href, children, className, style }: LinkProps) {
17
+ const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
18
+ // Allow default behavior for special clicks or external links
19
+ if (
20
+ e.metaKey || e.ctrlKey || e.shiftKey || e.button !== 0 ||
21
+ !href.startsWith('/') ||
22
+ e.currentTarget.hasAttribute('download')
23
+ ) {
24
+ return;
25
+ }
26
+
27
+ e.preventDefault();
28
+
29
+ // Use Hangar runtime's navigation function
30
+ if (typeof (window as any).melinaNavigate === 'function') {
31
+ (window as any).melinaNavigate(href);
32
+ } else {
33
+ // Fallback to regular navigation if runtime not loaded
34
+ console.warn('[Link] melinaNavigate not available, using direct navigation');
35
+ window.location.href = href;
36
+ }
37
+ };
38
+
39
+ return (
40
+ <a href={href} onClick={handleClick} className={className} style={style}>
41
+ {children}
42
+ </a>
43
+ );
44
+ }
45
+
46
+ // Hook for programmatic navigation
47
+ export function useNavigate() {
48
+ return (href: string) => {
49
+ if (typeof (window as any).melinaNavigate === 'function') {
50
+ (window as any).melinaNavigate(href);
51
+ } else {
52
+ window.location.href = href;
53
+ }
54
+ };
55
+ }
56
+
57
+ // Direct navigation function
58
+ export const clientNavigate = (href: string) => {
59
+ if (typeof (window as any).melinaNavigate === 'function') {
60
+ (window as any).melinaNavigate(href);
61
+ } else {
62
+ window.location.href = href;
63
+ }
64
+ };
65
+
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Client Component Registry
3
+ *
4
+ * This registry stores information about client components (islands).
5
+ * When a Server Component tries to render a Client Component,
6
+ * we render an island marker instead of the actual component.
7
+ */
8
+
9
+ import React from 'react';
10
+
11
+ // Registry of client component files
12
+ const clientComponents = new Map<string, Set<string>>();
13
+
14
+ /**
15
+ * Register a file as containing client components
16
+ */
17
+ export function registerClientComponent(filePath: string, exportNames: string[]) {
18
+ clientComponents.set(filePath, new Set(exportNames));
19
+ }
20
+
21
+ /**
22
+ * Check if a file is a client component
23
+ */
24
+ export function isClientComponent(filePath: string): boolean {
25
+ return clientComponents.has(filePath);
26
+ }
27
+
28
+ /**
29
+ * Create an island wrapper for a client component
30
+ * This renders the island marker on the server
31
+ */
32
+ export function createIslandWrapper<P extends object>(
33
+ componentName: string,
34
+ _OriginalComponent?: React.ComponentType<P>
35
+ ): React.FC<P> {
36
+ const IslandMarker: React.FC<P> = (props) => {
37
+ const propsJson = JSON.stringify(props).replace(/"/g, '&quot;');
38
+
39
+ // Return the island marker element
40
+ return React.createElement('div', {
41
+ 'data-melina-island': componentName,
42
+ 'data-props': propsJson,
43
+ children: React.createElement('div', {
44
+ style: {
45
+ padding: '1rem',
46
+ background: '#f0f0f0',
47
+ borderRadius: '4px',
48
+ opacity: 0.7
49
+ }
50
+ }, `Loading ${componentName}...`)
51
+ });
52
+ };
53
+
54
+ IslandMarker.displayName = `Island(${componentName})`;
55
+ return IslandMarker;
56
+ }
57
+
58
+ /**
59
+ * Transform a client component module for server-side use
60
+ * Replaces all exports with island wrappers
61
+ */
62
+ export function wrapClientModule(
63
+ originalModule: any,
64
+ componentNames: string[]
65
+ ): any {
66
+ const wrappedModule: any = {};
67
+
68
+ for (const name of componentNames) {
69
+ if (originalModule[name]) {
70
+ wrappedModule[name] = createIslandWrapper(name, originalModule[name]);
71
+ }
72
+ }
73
+
74
+ // Handle default export
75
+ if (originalModule.default) {
76
+ const defaultName = componentNames[0] || 'Default';
77
+ wrappedModule.default = createIslandWrapper(defaultName, originalModule.default);
78
+ }
79
+
80
+ return wrappedModule;
81
+ }
@@ -0,0 +1,121 @@
1
+ import React from 'react';
2
+
3
+ // Check if we're on the server
4
+ const isServer = typeof window === 'undefined';
5
+
6
+ // Global manifest of island chunks (populated by server)
7
+ declare global {
8
+ interface Window {
9
+ __MELINA_MANIFEST__?: Record<string, string>;
10
+ }
11
+ }
12
+
13
+ /**
14
+ * createIsland - Wrap a Client Component for automatic island hydration
15
+ *
16
+ * This is the bridge between Server and Client components in MelinaJS.
17
+ *
18
+ * Usage:
19
+ * ```tsx
20
+ * // components/Counter.tsx
21
+ * 'use client';
22
+ * import { createIsland } from '@ments/web';
23
+ *
24
+ * function CounterImpl({ initialCount = 0 }) {
25
+ * const [count, setCount] = useState(initialCount);
26
+ * return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
27
+ * }
28
+ *
29
+ * // Export the island-wrapped version
30
+ * export const Counter = createIsland(CounterImpl, 'Counter');
31
+ * ```
32
+ *
33
+ * On the Server:
34
+ * - Renders: <div data-melina-island="Counter" data-props='{"initialCount":0}'>...</div>
35
+ *
36
+ * On the Client:
37
+ * - Hydrates the component with full interactivity
38
+ */
39
+ export function createIsland<P extends object>(
40
+ Component: React.ComponentType<P>,
41
+ name: string
42
+ ): React.FC<P> {
43
+ // Return a wrapper component
44
+ const IslandWrapper: React.FC<P> = (props) => {
45
+ if (isServer) {
46
+ // SERVER: Render the island marker
47
+ // We also try to render the component for SEO/initial content
48
+ let innerHtml = '';
49
+ try {
50
+ // Attempt to render for progressive enhancement
51
+ // This might fail if the component uses hooks
52
+ const ReactDOMServer = require('react-dom/server');
53
+ innerHtml = ReactDOMServer.renderToString(
54
+ React.createElement(Component, props)
55
+ );
56
+ } catch (e) {
57
+ // Component uses hooks - can't SSR, just show placeholder
58
+ innerHtml = `<div class="melina-island-placeholder">Loading ${name}...</div>`;
59
+ }
60
+
61
+ // Return the island marker with serialized props
62
+ const propsJson = JSON.stringify(props).replace(/"/g, '&quot;');
63
+
64
+ return React.createElement('div', {
65
+ 'data-melina-island': name,
66
+ 'data-props': propsJson,
67
+ dangerouslySetInnerHTML: { __html: innerHtml }
68
+ });
69
+ } else {
70
+ // CLIENT: Render the actual component
71
+ return React.createElement(Component, props);
72
+ }
73
+ };
74
+
75
+ IslandWrapper.displayName = `Island(${name})`;
76
+ return IslandWrapper;
77
+ }
78
+
79
+ /**
80
+ * Island component - Alternative syntax for rendering islands
81
+ *
82
+ * Usage:
83
+ * ```tsx
84
+ * // In a Server Component
85
+ * import { Island } from '@ments/web';
86
+ * import { Counter } from './components/Counter';
87
+ *
88
+ * export default function Page() {
89
+ * return <Island component={Counter} name="Counter" initialCount={5} />;
90
+ * }
91
+ * ```
92
+ */
93
+ export function Island<P extends object>({
94
+ component: Component,
95
+ name,
96
+ ...props
97
+ }: {
98
+ component: React.ComponentType<P>;
99
+ name: string;
100
+ } & P): React.ReactElement {
101
+ const WrappedComponent = createIsland(Component, name);
102
+ return React.createElement(WrappedComponent, props as P);
103
+ }
104
+
105
+ /**
106
+ * ClientOnly - Render children only on the client
107
+ *
108
+ * Useful for components that absolutely cannot be SSR'd
109
+ */
110
+ export function ClientOnly({
111
+ children,
112
+ fallback = null
113
+ }: {
114
+ children: React.ReactNode;
115
+ fallback?: React.ReactNode;
116
+ }): React.ReactElement | null {
117
+ if (isServer) {
118
+ return fallback as React.ReactElement | null;
119
+ }
120
+ return children as React.ReactElement;
121
+ }
package/src/island.ts ADDED
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Island Helper - Automatic SSR detection and island marker rendering
3
+ *
4
+ * This is used internally by client components to automatically render
5
+ * as island markers when used in Server Components.
6
+ *
7
+ * Usage in a client component:
8
+ * ```tsx
9
+ * 'use client';
10
+ * import { island } from '@ments/web';
11
+ *
12
+ * function SearchBarImpl() {
13
+ * const [query, setQuery] = useState('');
14
+ * return <input value={query} onChange={e => setQuery(e.target.value)} />;
15
+ * }
16
+ *
17
+ * // Export the island-wrapped version
18
+ * export const SearchBar = island(SearchBarImpl, 'SearchBar');
19
+ * ```
20
+ *
21
+ * Or simpler - just export normally and the framework detects 'use client':
22
+ * ```tsx
23
+ * 'use client';
24
+ *
25
+ * export function SearchBar() {
26
+ * const [query, setQuery] = useState('');
27
+ * return <input value={query} onChange={e => setQuery(e.target.value)} />;
28
+ * }
29
+ * ```
30
+ */
31
+
32
+ import React from 'react';
33
+
34
+ const isServer = typeof window === 'undefined';
35
+
36
+ /**
37
+ * Wrap a component to auto-detect SSR and render island marker
38
+ */
39
+ export function island<P extends object>(
40
+ Component: React.ComponentType<P>,
41
+ name: string
42
+ ): React.FC<P> {
43
+ const IslandComponent: React.FC<P> = (props) => {
44
+ if (isServer) {
45
+ // SERVER: Render EMPTY placeholder - the Hangar portal will fill it
46
+ // We do NOT SSR the component because:
47
+ // 1. It may have view-transition-name which would conflict with portal content
48
+ // 2. The component may use client-only APIs (window.location, etc.)
49
+ // 3. The portal will render immediately on hydration anyway
50
+ const propsJson = JSON.stringify(props).replace(/"/g, '&quot;');
51
+
52
+ return React.createElement('div', {
53
+ 'data-melina-island': name,
54
+ 'data-props': propsJson,
55
+ style: { display: 'contents' } // No layout impact
56
+ });
57
+ // No children - portal will fill this
58
+ }
59
+
60
+ // CLIENT: Render actual component
61
+ return React.createElement(Component, props);
62
+ };
63
+
64
+ IslandComponent.displayName = `Island(${name})`;
65
+ return IslandComponent;
66
+ }
67
+
68
+ // Re-export for convenience
69
+ export { island as createIsland };