what-server 0.5.3 → 0.5.5

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/README.md ADDED
@@ -0,0 +1,136 @@
1
+ # what-server
2
+
3
+ Server-side rendering, streaming, islands architecture, and static site generation for [What Framework](https://whatfw.com). Zero JavaScript shipped by default -- islands opt in to client interactivity.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install what-server what-core
9
+ ```
10
+
11
+ Or use via the main package:
12
+
13
+ ```js
14
+ import { renderToString, renderToStream } from 'what-framework/server';
15
+ ```
16
+
17
+ ## Render to String
18
+
19
+ ```js
20
+ import { renderToString } from 'what-server';
21
+ import { h } from 'what-core';
22
+
23
+ function App() {
24
+ return h('div', null,
25
+ h('h1', null, 'Hello from the server'),
26
+ h('p', null, 'This page ships zero JavaScript.')
27
+ );
28
+ }
29
+
30
+ const html = renderToString(h(App));
31
+ // <div><h1>Hello from the server</h1><p>This page ships zero JavaScript.</p></div>
32
+ ```
33
+
34
+ ## Streaming SSR
35
+
36
+ ```js
37
+ import { renderToStream } from 'what-server';
38
+
39
+ for await (const chunk of renderToStream(h(App))) {
40
+ response.write(chunk);
41
+ }
42
+ response.end();
43
+ ```
44
+
45
+ ## Page Modes
46
+
47
+ ```js
48
+ import { definePage } from 'what-server';
49
+
50
+ export const page = definePage({
51
+ mode: 'static', // Pre-render at build time (default)
52
+ // mode: 'server' // Render on each request
53
+ // mode: 'client' // Render in browser (SPA)
54
+ // mode: 'hybrid' // Static shell + interactive islands
55
+ });
56
+ ```
57
+
58
+ ## Static Generation
59
+
60
+ ```js
61
+ import { generateStaticPage } from 'what-server';
62
+
63
+ const html = generateStaticPage({
64
+ component: App,
65
+ title: 'My Page',
66
+ meta: { description: 'A statically generated page' },
67
+ mode: 'static',
68
+ });
69
+ ```
70
+
71
+ ## Server Components
72
+
73
+ Mark components as server-only. They render on the server and ship no JavaScript to the client.
74
+
75
+ ```js
76
+ import { server } from 'what-server';
77
+
78
+ const Header = server(({ title }) => h('header', null, title));
79
+ ```
80
+
81
+ ## Server Actions
82
+
83
+ ```js
84
+ import { action, useAction, formAction, useFormAction } from 'what-server/actions';
85
+
86
+ // Define a server action
87
+ const addTodo = action(async (text) => {
88
+ await db.todos.create({ text });
89
+ });
90
+
91
+ // Use in a component
92
+ function TodoForm() {
93
+ const { execute, isPending } = useAction(addTodo);
94
+ return h('button', { onclick: () => execute('New todo') }, 'Add');
95
+ }
96
+
97
+ // Form action
98
+ const submitForm = formAction(async (formData) => {
99
+ const email = formData.get('email');
100
+ await subscribe(email);
101
+ });
102
+ ```
103
+
104
+ ## Sub-path Exports
105
+
106
+ | Path | Contents |
107
+ |---|---|
108
+ | `what-server` | `renderToString`, `renderToStream`, `definePage`, `generateStaticPage`, `server` |
109
+ | `what-server/islands` | Islands hydration runtime |
110
+ | `what-server/actions` | Server actions and mutations |
111
+
112
+ ## API
113
+
114
+ | Export | Description |
115
+ |---|---|
116
+ | `renderToString(vnode)` | Render a component tree to an HTML string |
117
+ | `renderToStream(vnode)` | Render as an async iterator for streaming |
118
+ | `definePage(config)` | Define page rendering mode and metadata |
119
+ | `generateStaticPage(page, data?)` | Generate a full HTML document |
120
+ | `server(Component)` | Mark a component as server-only |
121
+ | `action(fn)` | Define a server action |
122
+ | `formAction(fn)` | Define a form-based server action |
123
+ | `useAction(action)` | Hook to call a server action |
124
+ | `useFormAction(action)` | Hook for form server actions |
125
+ | `useOptimistic(state)` | Optimistic UI updates |
126
+ | `useMutation(fn)` | Mutation with loading/error states |
127
+ | `invalidatePath(path)` | Revalidate a page path |
128
+
129
+ ## Links
130
+
131
+ - [Documentation](https://whatfw.com)
132
+ - [GitHub](https://github.com/CelsianJs/whatfw)
133
+
134
+ ## License
135
+
136
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "what-server",
3
- "version": "0.5.3",
3
+ "version": "0.5.5",
4
4
  "description": "What Framework - SSR, islands architecture, static generation",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -29,17 +29,17 @@
29
29
  "server-rendering",
30
30
  "what-framework"
31
31
  ],
32
- "author": "",
32
+ "author": "ZVN DEV (https://zvndev.com)",
33
33
  "license": "MIT",
34
34
  "peerDependencies": {
35
35
  "what-core": "^0.5.3"
36
36
  },
37
37
  "repository": {
38
38
  "type": "git",
39
- "url": "https://github.com/zvndev/what-fw"
39
+ "url": "https://github.com/CelsianJs/whatfw"
40
40
  },
41
41
  "bugs": {
42
- "url": "https://github.com/zvndev/what-fw/issues"
42
+ "url": "https://github.com/CelsianJs/whatfw/issues"
43
43
  },
44
- "homepage": "https://whatframework.dev"
44
+ "homepage": "https://whatfw.com"
45
45
  }
package/src/actions.js CHANGED
@@ -17,7 +17,6 @@ import { signal, batch } from 'what-core';
17
17
 
18
18
  // Registry of server actions
19
19
  const actionRegistry = new Map();
20
- let actionIdCounter = 0;
21
20
 
22
21
  // --- CSRF Protection ---
23
22
  // Server generates a token per session; client sends it with every action request.
@@ -170,11 +169,15 @@ export function formAction(actionFn, options = {}) {
170
169
  formData = formDataOrEvent;
171
170
  }
172
171
 
173
- // Convert FormData to plain object
172
+ // Convert FormData to plain object, preserving File instances
174
173
  const data = {};
174
+ let hasFiles = false;
175
175
  for (const [key, value] of formData.entries()) {
176
+ if (typeof File !== 'undefined' && value instanceof File) {
177
+ hasFiles = true;
178
+ }
176
179
  if (data[key]) {
177
- // Handle multiple values (e.g., checkboxes)
180
+ // Handle multiple values (e.g., checkboxes, multi-file inputs)
178
181
  if (Array.isArray(data[key])) {
179
182
  data[key].push(value);
180
183
  } else {
@@ -186,7 +189,11 @@ export function formAction(actionFn, options = {}) {
186
189
  }
187
190
 
188
191
  try {
189
- const result = await actionFn(data);
192
+ // If form contains files, pass the raw FormData as second arg
193
+ // so the action handler can access files directly
194
+ const result = hasFiles
195
+ ? await actionFn(data, formData)
196
+ : await actionFn(data);
190
197
  if (onSuccess) onSuccess(result, form);
191
198
  if (resetOnSuccess && form) form.reset();
192
199
  return result;
package/src/index.js CHANGED
@@ -15,6 +15,23 @@ export function renderToString(vnode) {
15
15
  return escapeHtml(String(vnode));
16
16
  }
17
17
 
18
+ // Signal — unwrap by calling it
19
+ if (typeof vnode === 'function' && vnode._signal) {
20
+ return renderToString(vnode());
21
+ }
22
+
23
+ // Reactive function child — call to get value
24
+ if (typeof vnode === 'function') {
25
+ try {
26
+ return renderToString(vnode());
27
+ } catch (e) {
28
+ if (typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production') {
29
+ console.warn('[what-server] Error rendering reactive function in SSR:', e.message);
30
+ }
31
+ return '';
32
+ }
33
+ }
34
+
18
35
  // Array
19
36
  if (Array.isArray(vnode)) {
20
37
  return vnode.map(renderToString).join('');
@@ -52,6 +69,24 @@ export async function* renderToStream(vnode) {
52
69
  return;
53
70
  }
54
71
 
72
+ // Signal — unwrap by calling it
73
+ if (typeof vnode === 'function' && vnode._signal) {
74
+ yield* renderToStream(vnode());
75
+ return;
76
+ }
77
+
78
+ // Reactive function child — call to get value
79
+ if (typeof vnode === 'function') {
80
+ try {
81
+ yield* renderToStream(vnode());
82
+ } catch (e) {
83
+ if (typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production') {
84
+ console.warn('[what-server] Error rendering reactive function in stream SSR:', e.message);
85
+ }
86
+ }
87
+ return;
88
+ }
89
+
55
90
  if (Array.isArray(vnode)) {
56
91
  for (const child of vnode) {
57
92
  yield* renderToStream(child);
@@ -60,10 +95,17 @@ export async function* renderToStream(vnode) {
60
95
  }
61
96
 
62
97
  if (typeof vnode.tag === 'function') {
63
- const result = vnode.tag({ ...vnode.props, children: vnode.children });
64
- // Support async components
65
- const resolved = result instanceof Promise ? await result : result;
66
- yield* renderToStream(resolved);
98
+ try {
99
+ const result = vnode.tag({ ...vnode.props, children: vnode.children });
100
+ // Support async components
101
+ const resolved = result instanceof Promise ? await result : result;
102
+ yield* renderToStream(resolved);
103
+ } catch (e) {
104
+ if (typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production') {
105
+ console.warn('[what-server] Error rendering component in stream SSR:', e.message);
106
+ }
107
+ yield `<!-- SSR Error: ${escapeHtml(e.message || 'Component error')} -->`;
108
+ }
67
109
  return;
68
110
  }
69
111
 
@@ -181,7 +223,12 @@ function renderAttrs(props) {
181
223
  .join(';');
182
224
  out += ` style="${escapeHtml(css)}"`;
183
225
  } else if (val === true) {
184
- out += ` ${key}`;
226
+ // ARIA attributes require explicit ="true", HTML boolean attrs can be bare
227
+ if (key.startsWith('aria-') || key === 'role') {
228
+ out += ` ${key}="true"`;
229
+ } else {
230
+ out += ` ${key}`;
231
+ }
185
232
  } else {
186
233
  out += ` ${key}="${escapeHtml(String(val))}"`;
187
234
  }
@@ -199,6 +246,7 @@ function escapeHtml(str) {
199
246
  }
200
247
 
201
248
  function camelToKebab(str) {
249
+ if (str.startsWith('--')) return str; // CSS custom properties (variables) — leave unchanged
202
250
  return str.replace(/([A-Z])/g, '-$1').toLowerCase();
203
251
  }
204
252
 
@@ -219,4 +267,7 @@ export {
219
267
  invalidatePath,
220
268
  handleActionRequest,
221
269
  getRegisteredActions,
270
+ generateCsrfToken,
271
+ validateCsrfToken,
272
+ csrfMetaTag,
222
273
  } from './actions.js';