what-server 0.6.0 → 0.6.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/dist/actions.js +2 -1
- package/dist/actions.js.map +2 -2
- package/dist/actions.min.js +1 -1
- package/dist/actions.min.js.map +3 -3
- package/dist/index.js +87 -14
- package/dist/index.js.map +2 -2
- package/dist/index.min.js +14 -11
- package/dist/index.min.js.map +3 -3
- package/index.d.ts +12 -0
- package/package.json +2 -2
- package/src/actions.js +6 -2
- package/src/index.js +113 -13
package/src/actions.js
CHANGED
|
@@ -76,11 +76,15 @@ export function csrfMetaTag(token) {
|
|
|
76
76
|
|
|
77
77
|
// --- Define a server action ---
|
|
78
78
|
|
|
79
|
+
let _actionCounter = 0;
|
|
80
|
+
|
|
79
81
|
function generateActionId() {
|
|
80
|
-
// Generate a
|
|
82
|
+
// Generate a deterministic ID — prefer crypto.getRandomValues, fall back to a
|
|
83
|
+
// monotonic counter (never Math.random, which is not cryptographically safe and
|
|
84
|
+
// produces predictable IDs in some runtimes).
|
|
81
85
|
const rand = typeof crypto !== 'undefined' && crypto.getRandomValues
|
|
82
86
|
? Array.from(crypto.getRandomValues(new Uint8Array(6)), b => b.toString(16).padStart(2, '0')).join('')
|
|
83
|
-
:
|
|
87
|
+
: `c${(++_actionCounter).toString(36)}_${Date.now().toString(36)}`;
|
|
84
88
|
return `a_${rand}`;
|
|
85
89
|
}
|
|
86
90
|
|
package/src/index.js
CHANGED
|
@@ -4,6 +4,72 @@
|
|
|
4
4
|
|
|
5
5
|
import { h } from 'what-core';
|
|
6
6
|
|
|
7
|
+
// --- SSR Error Collection ---
|
|
8
|
+
// Errors that occur during SSR are collected and serialized into the HTML output
|
|
9
|
+
// so the client can pick them up during hydration and display/report them.
|
|
10
|
+
|
|
11
|
+
let _ssrErrors = [];
|
|
12
|
+
const MAX_SSR_ERRORS = 50;
|
|
13
|
+
|
|
14
|
+
function _collectSSRError(error, context = {}) {
|
|
15
|
+
const entry = {
|
|
16
|
+
code: error.code || 'ERR_SSR_RENDER',
|
|
17
|
+
message: error.message || String(error),
|
|
18
|
+
component: context.component || null,
|
|
19
|
+
timestamp: Date.now(),
|
|
20
|
+
};
|
|
21
|
+
// In dev mode, include extra detail for debugging
|
|
22
|
+
if (_isDevMode) {
|
|
23
|
+
entry.suggestion = error.suggestion || null;
|
|
24
|
+
entry.stack = error.stack?.split('\n').slice(0, 5).join('\n') || null;
|
|
25
|
+
}
|
|
26
|
+
_ssrErrors.push(entry);
|
|
27
|
+
if (_ssrErrors.length > MAX_SSR_ERRORS) _ssrErrors.shift();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function _resetSSRErrors() {
|
|
31
|
+
_ssrErrors = [];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Serialize collected SSR errors into a script tag for client hydration.
|
|
36
|
+
* In dev mode: includes full error details (message, suggestion, stack).
|
|
37
|
+
* In production: includes only error code and component name.
|
|
38
|
+
*/
|
|
39
|
+
export function serializeSSRErrors() {
|
|
40
|
+
if (_ssrErrors.length === 0) return '';
|
|
41
|
+
const payload = _isDevMode
|
|
42
|
+
? _ssrErrors
|
|
43
|
+
: _ssrErrors.map(e => ({ code: e.code, component: e.component }));
|
|
44
|
+
const json = JSON.stringify(payload).replace(/<\//g, '<\\/'); // prevent XSS via </script>
|
|
45
|
+
return `<script type="application/json" data-what-ssr-errors>${json}</script>`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Read SSR errors from the DOM during client hydration.
|
|
50
|
+
* Call this on the client side during hydration to pick up errors from SSR.
|
|
51
|
+
* Returns an array of error objects, or empty array if none.
|
|
52
|
+
*/
|
|
53
|
+
export function hydrateSSRErrors() {
|
|
54
|
+
if (typeof document === 'undefined') return [];
|
|
55
|
+
const el = document.querySelector('script[data-what-ssr-errors]');
|
|
56
|
+
if (!el) return [];
|
|
57
|
+
try {
|
|
58
|
+
const errors = JSON.parse(el.textContent);
|
|
59
|
+
el.remove(); // clean up after reading
|
|
60
|
+
return errors;
|
|
61
|
+
} catch {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get collected SSR errors (for programmatic access before serialization).
|
|
68
|
+
*/
|
|
69
|
+
export function getSSRErrors() {
|
|
70
|
+
return _ssrErrors.slice();
|
|
71
|
+
}
|
|
72
|
+
|
|
7
73
|
// --- Hydration ID Generator ---
|
|
8
74
|
let _hydrationIdCounter = 0;
|
|
9
75
|
|
|
@@ -21,6 +87,7 @@ function nextHydrationId() {
|
|
|
21
87
|
|
|
22
88
|
export function renderToHydratableString(vnode) {
|
|
23
89
|
resetHydrationId();
|
|
90
|
+
_resetSSRErrors();
|
|
24
91
|
return _renderHydratable(vnode);
|
|
25
92
|
}
|
|
26
93
|
|
|
@@ -42,7 +109,8 @@ function _renderHydratable(vnode) {
|
|
|
42
109
|
try {
|
|
43
110
|
return `<!--$-->${_renderHydratable(vnode())}<!--/$-->`;
|
|
44
111
|
} catch (e) {
|
|
45
|
-
|
|
112
|
+
_collectSSRError(e, { component: 'reactive-function' });
|
|
113
|
+
if (_isDevMode) {
|
|
46
114
|
console.warn('[what-server] Error rendering reactive function in SSR:', e.message);
|
|
47
115
|
}
|
|
48
116
|
return '<!--$--><!--/$-->';
|
|
@@ -57,10 +125,20 @@ function _renderHydratable(vnode) {
|
|
|
57
125
|
// Component — add hydration key to root element
|
|
58
126
|
if (typeof vnode.tag === 'function') {
|
|
59
127
|
const hkId = nextHydrationId();
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
128
|
+
const componentName = vnode.tag.displayName || vnode.tag.name || 'Anonymous';
|
|
129
|
+
try {
|
|
130
|
+
const result = vnode.tag({ ...vnode.props, children: vnode.children });
|
|
131
|
+
const html = _renderHydratable(result);
|
|
132
|
+
// Inject data-hk into the first element tag if present
|
|
133
|
+
return injectHydrationKey(html, hkId);
|
|
134
|
+
} catch (e) {
|
|
135
|
+
_collectSSRError(e, { component: componentName });
|
|
136
|
+
if (_isDevMode) {
|
|
137
|
+
console.warn(`[what-server] Error rendering component "${componentName}" in SSR:`, e.message);
|
|
138
|
+
return `<!--ssr-error:${escapeHtml(componentName)}-->`;
|
|
139
|
+
}
|
|
140
|
+
return `<!--ssr-error-->`;
|
|
141
|
+
}
|
|
64
142
|
}
|
|
65
143
|
|
|
66
144
|
// Element
|
|
@@ -110,7 +188,8 @@ export function renderToString(vnode) {
|
|
|
110
188
|
try {
|
|
111
189
|
return renderToString(vnode());
|
|
112
190
|
} catch (e) {
|
|
113
|
-
|
|
191
|
+
_collectSSRError(e, { component: 'reactive-function' });
|
|
192
|
+
if (_isDevMode) {
|
|
114
193
|
console.warn('[what-server] Error rendering reactive function in SSR:', e.message);
|
|
115
194
|
}
|
|
116
195
|
return '';
|
|
@@ -124,8 +203,18 @@ export function renderToString(vnode) {
|
|
|
124
203
|
|
|
125
204
|
// Component
|
|
126
205
|
if (typeof vnode.tag === 'function') {
|
|
127
|
-
const
|
|
128
|
-
|
|
206
|
+
const componentName = vnode.tag.displayName || vnode.tag.name || 'Anonymous';
|
|
207
|
+
try {
|
|
208
|
+
const result = vnode.tag({ ...vnode.props, children: vnode.children });
|
|
209
|
+
return renderToString(result);
|
|
210
|
+
} catch (e) {
|
|
211
|
+
_collectSSRError(e, { component: componentName });
|
|
212
|
+
if (_isDevMode) {
|
|
213
|
+
console.warn(`[what-server] Error rendering component "${componentName}" in SSR:`, e.message);
|
|
214
|
+
return `<!-- SSR Error in ${escapeHtml(componentName)}: ${escapeHtml(e.message)} -->`;
|
|
215
|
+
}
|
|
216
|
+
return `<!-- SSR Error -->`;
|
|
217
|
+
}
|
|
129
218
|
}
|
|
130
219
|
|
|
131
220
|
// Element
|
|
@@ -163,7 +252,8 @@ export async function* renderToStream(vnode) {
|
|
|
163
252
|
try {
|
|
164
253
|
yield* renderToStream(vnode());
|
|
165
254
|
} catch (e) {
|
|
166
|
-
|
|
255
|
+
_collectSSRError(e, { component: 'reactive-function' });
|
|
256
|
+
if (_isDevMode) {
|
|
167
257
|
console.warn('[what-server] Error rendering reactive function in stream SSR:', e.message);
|
|
168
258
|
}
|
|
169
259
|
}
|
|
@@ -178,17 +268,19 @@ export async function* renderToStream(vnode) {
|
|
|
178
268
|
}
|
|
179
269
|
|
|
180
270
|
if (typeof vnode.tag === 'function') {
|
|
271
|
+
const componentName = vnode.tag.displayName || vnode.tag.name || 'Anonymous';
|
|
181
272
|
try {
|
|
182
273
|
const result = vnode.tag({ ...vnode.props, children: vnode.children });
|
|
183
274
|
// Support async components
|
|
184
275
|
const resolved = result instanceof Promise ? await result : result;
|
|
185
276
|
yield* renderToStream(resolved);
|
|
186
277
|
} catch (e) {
|
|
187
|
-
|
|
188
|
-
|
|
278
|
+
_collectSSRError(e, { component: componentName });
|
|
279
|
+
if (_isDevMode) {
|
|
280
|
+
console.warn(`[what-server] Error rendering component "${componentName}" in stream SSR:`, e.message);
|
|
189
281
|
}
|
|
190
282
|
yield _isDevMode
|
|
191
|
-
? `<!-- SSR Error: ${escapeHtml(e.message || 'Component error')} -->`
|
|
283
|
+
? `<!-- SSR Error in ${escapeHtml(componentName)}: ${escapeHtml(e.message || 'Component error')} -->`
|
|
192
284
|
: `<!-- SSR Error -->`;
|
|
193
285
|
}
|
|
194
286
|
return;
|
|
@@ -226,6 +318,7 @@ export function definePage(config) {
|
|
|
226
318
|
|
|
227
319
|
// Generate static HTML for a page
|
|
228
320
|
export function generateStaticPage(page, data = {}) {
|
|
321
|
+
_resetSSRErrors();
|
|
229
322
|
const vnode = page.component(data);
|
|
230
323
|
const html = renderToString(vnode);
|
|
231
324
|
const islands = page.islands || [];
|
|
@@ -238,10 +331,11 @@ export function generateStaticPage(page, data = {}) {
|
|
|
238
331
|
scripts: page.mode === 'static' ? [] : page.scripts || [],
|
|
239
332
|
styles: page.styles || [],
|
|
240
333
|
mode: page.mode,
|
|
334
|
+
ssrErrors: serializeSSRErrors(),
|
|
241
335
|
});
|
|
242
336
|
}
|
|
243
337
|
|
|
244
|
-
function wrapDocument({ title, meta, body, islands, scripts, styles, mode }) {
|
|
338
|
+
function wrapDocument({ title, meta, body, islands, scripts, styles, mode, ssrErrors = '' }) {
|
|
245
339
|
const metaTags = Object.entries(meta)
|
|
246
340
|
.map(([name, content]) => `<meta name="${escapeHtml(name)}" content="${escapeHtml(content)}">`)
|
|
247
341
|
.join('\n ');
|
|
@@ -274,6 +368,7 @@ function wrapDocument({ title, meta, body, islands, scripts, styles, mode }) {
|
|
|
274
368
|
</head>
|
|
275
369
|
<body>
|
|
276
370
|
<div id="app">${body}</div>
|
|
371
|
+
${ssrErrors}
|
|
277
372
|
${islandScript}
|
|
278
373
|
${scriptTags}
|
|
279
374
|
${clientScript}
|
|
@@ -375,6 +470,11 @@ const VOID_ELEMENTS = new Set([
|
|
|
375
470
|
'link', 'meta', 'param', 'source', 'track', 'wbr',
|
|
376
471
|
]);
|
|
377
472
|
|
|
473
|
+
// SSR error serialization is exported above:
|
|
474
|
+
// serializeSSRErrors() — serialize collected errors to script tag
|
|
475
|
+
// hydrateSSRErrors() — read errors from DOM during client hydration
|
|
476
|
+
// getSSRErrors() — programmatic access to collected errors
|
|
477
|
+
|
|
378
478
|
// Re-export server actions
|
|
379
479
|
export {
|
|
380
480
|
action,
|