melina 1.1.2 → 1.1.3

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/bin/melina CHANGED
File without changes
package/package.json CHANGED
@@ -1,76 +1,76 @@
1
- {
2
- "name": "melina",
3
- "version": "1.1.2",
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
- "CHANGELOG.md",
17
- "CONTRIBUTING.md",
18
- "ARCHITECTURE.md"
19
- ],
20
- "exports": {
21
- ".": "./src/web.ts",
22
- "./web": "./src/web.ts",
23
- "./Link": "./src/Link.tsx",
24
- "./island": "./src/island.ts",
25
- "./client": "./src/client.ts",
26
- "./navigation": "./src/runtime/navigation.tsx",
27
- "./client/jsx-runtime": "./src/client.ts",
28
- "./client/jsx-dev-runtime": "./src/client.ts"
29
- },
30
- "repository": {
31
- "type": "git",
32
- "url": "https://github.com/mements/melina.js"
33
- },
34
- "author": {
35
- "name": "Mements Team"
36
- },
37
- "bugs": "https://github.com/mements/melina.js/issues",
38
- "homepage": "https://github.com/mements/melina.js#readme",
39
- "license": "MIT",
40
- "keywords": [
41
- "bun",
42
- "server",
43
- "framework",
44
- "typescript",
45
- "web",
46
- "ssr",
47
- "islands",
48
- "islands-architecture",
49
- "hydration",
50
- "nextjs",
51
- "app-router",
52
- "react",
53
- "partial-hydration"
54
- ],
55
- "type": "module",
56
- "private": false,
57
- "sideEffects": false,
58
- "devDependencies": {
59
- "@types/bun": "latest",
60
- "@types/react": "^19.2.10",
61
- "@types/react-dom": "^19.2.3"
62
- },
63
- "dependencies": {
64
- "@ments/utils": "^1.2.1",
65
- "@modelcontextprotocol/sdk": "^1.11.0",
66
- "@solana/kit": "^2.1.0",
67
- "@tailwindcss/postcss": "^4.1.10",
68
- "autoprefixer": "^10.4.21",
69
- "postcss": "^8.5.6",
70
- "react": "^19.1.1",
71
- "react-dom": "^19.1.1",
72
- "ts-dedent": "^2.2.0",
73
- "html-react-parser": "^5.2.5",
74
- "zod": "^3.24.4"
75
- }
1
+ {
2
+ "name": "melina",
3
+ "version": "1.1.3",
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
+ "CHANGELOG.md",
17
+ "CONTRIBUTING.md",
18
+ "ARCHITECTURE.md"
19
+ ],
20
+ "exports": {
21
+ ".": "./src/web.ts",
22
+ "./web": "./src/web.ts",
23
+ "./Link": "./src/Link.tsx",
24
+ "./island": "./src/island.ts",
25
+ "./client": "./src/client.ts",
26
+ "./navigation": "./src/runtime/navigation.tsx",
27
+ "./client/jsx-runtime": "./src/client.ts",
28
+ "./client/jsx-dev-runtime": "./src/client.ts"
29
+ },
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "https://github.com/mements/melina.js"
33
+ },
34
+ "author": {
35
+ "name": "Mements Team"
36
+ },
37
+ "bugs": "https://github.com/mements/melina.js/issues",
38
+ "homepage": "https://github.com/mements/melina.js#readme",
39
+ "license": "MIT",
40
+ "keywords": [
41
+ "bun",
42
+ "server",
43
+ "framework",
44
+ "typescript",
45
+ "web",
46
+ "ssr",
47
+ "islands",
48
+ "islands-architecture",
49
+ "hydration",
50
+ "nextjs",
51
+ "app-router",
52
+ "react",
53
+ "partial-hydration"
54
+ ],
55
+ "type": "module",
56
+ "private": false,
57
+ "sideEffects": false,
58
+ "devDependencies": {
59
+ "@types/bun": "latest",
60
+ "@types/react": "^19.2.10",
61
+ "@types/react-dom": "^19.2.3"
62
+ },
63
+ "dependencies": {
64
+ "@ments/utils": "^1.2.1",
65
+ "@modelcontextprotocol/sdk": "^1.11.0",
66
+ "@solana/kit": "^2.1.0",
67
+ "@tailwindcss/postcss": "^4.1.10",
68
+ "autoprefixer": "^10.4.21",
69
+ "postcss": "^8.5.6",
70
+ "react": "^19.1.1",
71
+ "react-dom": "^19.1.1",
72
+ "ts-dedent": "^2.2.0",
73
+ "html-react-parser": "^5.2.5",
74
+ "zod": "^3.24.4"
75
+ }
76
76
  }
package/src/client.ts CHANGED
@@ -683,6 +683,115 @@ export function jsxDEV(
683
683
  };
684
684
  }
685
685
 
686
+ // =============================================================================
687
+ // SERVER-SIDE RENDERING
688
+ // =============================================================================
689
+
690
+ /**
691
+ * Render a VNode tree to an HTML string (SSR)
692
+ * This is the React-free equivalent of ReactDOMServer.renderToString()
693
+ */
694
+ export function renderToString(vnode: VNode | string | number | boolean | null | undefined): string {
695
+ if (vnode == null || typeof vnode === 'boolean') {
696
+ return '';
697
+ }
698
+
699
+ if (typeof vnode === 'string') {
700
+ return escapeHtml(vnode);
701
+ }
702
+
703
+ if (typeof vnode === 'number') {
704
+ return String(vnode);
705
+ }
706
+
707
+ const { type, props } = vnode;
708
+
709
+ // Handle Fragment
710
+ if (type === Fragment) {
711
+ return renderChildren(props.children);
712
+ }
713
+
714
+ // Handle function components
715
+ if (typeof type === 'function') {
716
+ const result = type(props);
717
+ return renderToString(result);
718
+ }
719
+
720
+ // Handle HTML elements
721
+ const tagName = type as string;
722
+ const attrs = renderAttributes(props);
723
+ const children = renderChildren(props.children);
724
+
725
+ // Void elements (self-closing)
726
+ const voidElements = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr'];
727
+ if (voidElements.includes(tagName)) {
728
+ return `<${tagName}${attrs}>`;
729
+ }
730
+
731
+ return `<${tagName}${attrs}>${children}</${tagName}>`;
732
+ }
733
+
734
+ function escapeHtml(str: string): string {
735
+ return str
736
+ .replace(/&/g, '&amp;')
737
+ .replace(/</g, '&lt;')
738
+ .replace(/>/g, '&gt;')
739
+ .replace(/"/g, '&quot;')
740
+ .replace(/'/g, '&#39;');
741
+ }
742
+
743
+ function renderAttributes(props: Props): string {
744
+ const attrs: string[] = [];
745
+
746
+ for (const [key, value] of Object.entries(props)) {
747
+ if (key === 'children' || key === 'key' || key === 'ref') continue;
748
+ if (value == null || value === false) continue;
749
+
750
+ // Handle className -> class
751
+ const attrName = key === 'className' ? 'class' : key;
752
+
753
+ // Handle style object
754
+ if (key === 'style' && typeof value === 'object') {
755
+ const styleStr = Object.entries(value)
756
+ .map(([k, v]) => `${k.replace(/([A-Z])/g, '-$1').toLowerCase()}:${v}`)
757
+ .join(';');
758
+ attrs.push(`style="${escapeHtml(styleStr)}"`);
759
+ continue;
760
+ }
761
+
762
+ // Handle event handlers (skip on server)
763
+ if (key.startsWith('on') && typeof value === 'function') continue;
764
+
765
+ // Handle dangerouslySetInnerHTML (handled in children)
766
+ if (key === 'dangerouslySetInnerHTML') continue;
767
+
768
+ // Boolean attributes
769
+ if (value === true) {
770
+ attrs.push(attrName);
771
+ continue;
772
+ }
773
+
774
+ attrs.push(`${attrName}="${escapeHtml(String(value))}"`);
775
+ }
776
+
777
+ return attrs.length > 0 ? ' ' + attrs.join(' ') : '';
778
+ }
779
+
780
+ function renderChildren(children: Child | Child[] | undefined): string {
781
+ if (children == null) return '';
782
+
783
+ // Handle dangerouslySetInnerHTML
784
+ if (typeof children === 'object' && '__html' in (children as any)) {
785
+ return (children as any).__html;
786
+ }
787
+
788
+ if (Array.isArray(children)) {
789
+ return children.map(child => renderToString(child as VNode)).join('');
790
+ }
791
+
792
+ return renderToString(children as VNode);
793
+ }
794
+
686
795
  // =============================================================================
687
796
  // HOOKS
688
797
  // =============================================================================
@@ -706,12 +815,15 @@ function getHook<T extends HookState>(initializer: () => T): T {
706
815
  * useState - Reactive state
707
816
  */
708
817
  export function useState<T>(initial: T | (() => T)): [T, (v: T | ((prev: T) => T)) => void] {
818
+ // Capture the fiber at hook creation time (during render)
819
+ const fiber = currentFiber!;
820
+
709
821
  const hook = getHook(() => {
710
822
  const value = typeof initial === 'function' ? (initial as () => T)() : initial;
711
823
  return { type: 'state' as const, value, setter: null as any };
712
824
  });
713
825
 
714
- // Create setter that triggers re-render
826
+ // Create setter that triggers re-render using captured fiber
715
827
  if (!hook.setter) {
716
828
  hook.setter = (newValue: T | ((prev: T) => T)) => {
717
829
  const next = typeof newValue === 'function'
@@ -720,7 +832,8 @@ export function useState<T>(initial: T | (() => T)): [T, (v: T | ((prev: T) => T
720
832
 
721
833
  if (next !== hook.value) {
722
834
  hook.value = next;
723
- scheduleUpdate(currentFiber!);
835
+ // Use captured fiber, not currentFiber (which is null outside render)
836
+ scheduleUpdate(fiber);
724
837
  }
725
838
  };
726
839
  }
@@ -780,14 +893,41 @@ export function useCallback<T extends Function>(fn: T, deps: any[]): T {
780
893
  // =============================================================================
781
894
 
782
895
  function scheduleUpdate(fiber: Fiber) {
783
- // Find root fiber
784
- let root = fiber;
785
- while (root.parent) root = root.parent;
786
-
787
896
  // Schedule microtask re-render
788
897
  queueMicrotask(() => {
789
- if (fiber.vnode) {
790
- reconcile(fiber, fiber.vnode);
898
+ if (fiber.vnode && typeof fiber.vnode.type === 'function') {
899
+ // This is a component fiber - re-render it properly
900
+ const prevFiber = currentFiber;
901
+ currentFiber = fiber;
902
+ fiber.hookIndex = 0; // Reset hook index for re-render
903
+
904
+ // Re-run the component function (hooks will reuse stored state)
905
+ const result = (fiber.vnode.type as any)(fiber.vnode.props);
906
+
907
+ currentFiber = prevFiber;
908
+
909
+ // Update the DOM with new result
910
+ if (fiber.node && fiber.node.parentNode) {
911
+ const container = fiber.node.parentNode as HTMLElement;
912
+
913
+ // Create new DOM from result
914
+ const newFiber: Fiber = {
915
+ node: null,
916
+ vnode: result,
917
+ hooks: [],
918
+ hookIndex: 0,
919
+ parent: fiber.parent,
920
+ children: [],
921
+ cleanup: [],
922
+ };
923
+
924
+ const newNode = createNode(result, newFiber);
925
+ if (newNode) {
926
+ container.replaceChild(newNode, fiber.node);
927
+ fiber.node = newNode as HTMLElement;
928
+ }
929
+ }
930
+
791
931
  runEffects();
792
932
  }
793
933
  });
@@ -1204,15 +1344,11 @@ async function loadComponent(name: string): Promise<Component<any> | null> {
1204
1344
 
1205
1345
  /**
1206
1346
  * Hydrate all islands in the current DOM
1207
- * Uses ReactDOM.createRoot for React-based components
1347
+ * Uses melina/client's render function (React-free!)
1208
1348
  */
1209
1349
  export async function hydrateIslands(): Promise<void> {
1210
1350
  initHangar();
1211
1351
 
1212
- // Dynamic import of React and ReactDOM for island hydration
1213
- const React = await import('react');
1214
- const ReactDOM = await import('react-dom/client');
1215
-
1216
1352
  const placeholders = document.querySelectorAll('[data-melina-island]');
1217
1353
  const seenIds = new Set<string>();
1218
1354
 
@@ -1244,22 +1380,19 @@ export async function hydrateIslands(): Promise<void> {
1244
1380
  storageNode.setAttribute('data-storage', instanceId);
1245
1381
  el.appendChild(storageNode);
1246
1382
 
1247
- // Render component using ReactDOM.createRoot for proper React hydration
1248
- const root = ReactDOM.createRoot(storageNode);
1249
- root.render(React.createElement(Component as any, props));
1250
-
1251
- // Store root for cleanup and re-rendering
1252
- (storageNode as any).__reactRoot = root;
1383
+ // Render component using melina's render function (React-free!)
1384
+ const vnode = createElement(Component as any, props);
1385
+ render(vnode, storageNode);
1253
1386
 
1254
1387
  islandRegistry.set(instanceId, {
1255
1388
  name,
1256
1389
  Component,
1257
1390
  props,
1258
- fiber: null as any, // Not using fiber for React components
1391
+ fiber: null as any,
1259
1392
  storageNode,
1260
1393
  });
1261
1394
 
1262
- console.log('[Melina Client] Created React island:', instanceId);
1395
+ console.log('[Melina Client] Hydrated island:', instanceId);
1263
1396
  }
1264
1397
  }
1265
1398
  }
package/src/jsx-dom.ts ADDED
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Melina.js JSX-to-DOM Runtime
3
+ *
4
+ * JSX in client.tsx files creates REAL DOM elements, not virtual DOM.
5
+ *
6
+ * Usage:
7
+ * const el = <div class="toast"><span>Hello</span></div>;
8
+ * document.body.appendChild(el); // Works directly!
9
+ */
10
+
11
+ type Child = Node | string | number | boolean | null | undefined | Child[];
12
+
13
+ export function jsx(
14
+ tag: string | ((props: any) => Node),
15
+ props: Record<string, any> | null,
16
+ ...children: Child[]
17
+ ): Node {
18
+ // Function component
19
+ if (typeof tag === 'function') {
20
+ const finalProps = { ...props };
21
+ // Use varargs children only if props.children is missing (Classic Runtime fallback)
22
+ if ((!props || props.children === undefined) && children.length > 0) {
23
+ finalProps.children = children.length === 1 ? children[0] : children;
24
+ }
25
+ return tag(finalProps);
26
+ }
27
+
28
+ const el = document.createElement(tag);
29
+
30
+ // Set attributes/properties
31
+ if (props) {
32
+ for (const [key, value] of Object.entries(props)) {
33
+ if (key === 'children') continue;
34
+ if (value === null || value === undefined || value === false) continue;
35
+
36
+ if (key === 'style' && typeof value === 'object') {
37
+ Object.assign(el.style, value);
38
+ } else if (key === 'className' || key === 'class') {
39
+ el.className = String(value);
40
+ } else if (key === 'htmlFor') {
41
+ el.setAttribute('for', String(value));
42
+ } else if (key === 'dangerouslySetInnerHTML') {
43
+ el.innerHTML = value.__html || '';
44
+ } else if (key.startsWith('on') && typeof value === 'function') {
45
+ // Event handlers: onClick -> click
46
+ const event = key.slice(2).toLowerCase();
47
+ el.addEventListener(event, value);
48
+ } else if (key === 'ref' && typeof value === 'function') {
49
+ value(el);
50
+ } else if (value === true) {
51
+ el.setAttribute(key, '');
52
+ } else {
53
+ el.setAttribute(key, String(value));
54
+ }
55
+ }
56
+
57
+ // Handle children passed as prop
58
+ // Handle children passed as prop (Automatic Runtime)
59
+ if (props.children !== undefined) {
60
+ appendChildren(el, Array.isArray(props.children) ? props.children : [props.children]);
61
+ }
62
+ }
63
+
64
+ // Append direct children (Classic Runtime fallback)
65
+ if ((!props || props.children === undefined) && children.length > 0) {
66
+ appendChildren(el, children);
67
+ }
68
+
69
+ return el;
70
+ }
71
+
72
+ function appendChildren(parent: Element, children: Child[]) {
73
+ for (const child of children) {
74
+ if (child === null || child === undefined || child === false || child === true) continue;
75
+ if (Array.isArray(child)) {
76
+ appendChildren(parent, child);
77
+ } else if (child instanceof Node) {
78
+ parent.appendChild(child);
79
+ } else {
80
+ parent.appendChild(document.createTextNode(String(child)));
81
+ }
82
+ }
83
+ }
84
+
85
+ // JSX runtime entry points (used by Bun/esbuild JSX transform)
86
+ export const jsxs = jsx;
87
+ export const jsxDEV = jsx;
88
+ export const Fragment = ({ children }: { children: Child | Child[] }) => {
89
+ const frag = document.createDocumentFragment();
90
+ if (children !== undefined) {
91
+ appendChildren(frag as any, Array.isArray(children) ? children : [children]);
92
+ }
93
+ return frag;
94
+ };
95
+
96
+ export default { jsx, jsxs, jsxDEV, Fragment };