tova 0.1.1 → 0.2.2
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/LICENSE +1 -1
- package/README.md +2 -0
- package/bin/tova.js +811 -154
- package/package.json +8 -2
- package/src/analyzer/analyzer.js +297 -58
- package/src/analyzer/scope.js +38 -1
- package/src/analyzer/type-registry.js +72 -0
- package/src/analyzer/types.js +478 -0
- package/src/codegen/base-codegen.js +371 -0
- package/src/codegen/client-codegen.js +62 -10
- package/src/codegen/codegen.js +111 -2
- package/src/codegen/server-codegen.js +175 -3
- package/src/config/edit-toml.js +100 -0
- package/src/config/package-json.js +52 -0
- package/src/config/resolve.js +100 -0
- package/src/config/toml.js +209 -0
- package/src/lexer/lexer.js +2 -2
- package/src/lsp/server.js +284 -30
- package/src/parser/ast.js +105 -0
- package/src/parser/parser.js +202 -2
- package/src/runtime/ai.js +305 -0
- package/src/runtime/devtools.js +228 -0
- package/src/runtime/embedded.js +3 -1
- package/src/runtime/io.js +240 -0
- package/src/runtime/reactivity.js +264 -19
- package/src/runtime/ssr.js +196 -24
- package/src/runtime/table.js +522 -0
- package/src/stdlib/collections.js +245 -0
- package/src/stdlib/core.js +87 -0
- package/src/stdlib/datetime.js +88 -0
- package/src/stdlib/encoding.js +35 -0
- package/src/stdlib/functional.js +82 -0
- package/src/stdlib/inline.js +334 -67
- package/src/stdlib/math.js +93 -0
- package/src/stdlib/string.js +95 -0
- package/src/stdlib/url.js +33 -0
- package/src/stdlib/validation.js +29 -0
package/src/runtime/ssr.js
CHANGED
|
@@ -21,6 +21,51 @@ function escapeAttr(str) {
|
|
|
21
21
|
return str.replace(/&/g, '&').replace(/"/g, '"');
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
// ─── SSR ID Counter for hydration markers ─────────────────
|
|
25
|
+
let ssrIdCounter = 0;
|
|
26
|
+
|
|
27
|
+
function nextSSRId() {
|
|
28
|
+
return ++ssrIdCounter;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function resetSSRIdCounter() {
|
|
32
|
+
ssrIdCounter = 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ─── Render props to attribute string ─────────────────────
|
|
36
|
+
function renderPropsToString(props, vnode) {
|
|
37
|
+
let html = '';
|
|
38
|
+
for (const [key, value] of Object.entries(props || {})) {
|
|
39
|
+
if (key === 'key' || key === 'ref') continue;
|
|
40
|
+
if (key.startsWith('on')) continue; // skip event handlers in SSR
|
|
41
|
+
|
|
42
|
+
const val = typeof value === 'function' ? value() : value;
|
|
43
|
+
if (val === false || val == null) continue;
|
|
44
|
+
|
|
45
|
+
if (key === 'className') {
|
|
46
|
+
html += ` class="${escapeAttr(val)}"`;
|
|
47
|
+
} else if (key === 'style' && typeof val === 'object') {
|
|
48
|
+
const styleStr = Object.entries(val)
|
|
49
|
+
.map(([k, v]) => `${k.replace(/[A-Z]/g, m => '-' + m.toLowerCase())}:${v}`)
|
|
50
|
+
.join(';');
|
|
51
|
+
html += ` style="${escapeAttr(styleStr)}"`;
|
|
52
|
+
} else if (key === 'checked' || key === 'disabled' || key === 'selected' || key === 'readonly') {
|
|
53
|
+
if (val) html += ` ${key}`;
|
|
54
|
+
} else if (key === 'value') {
|
|
55
|
+
html += ` value="${escapeAttr(val)}"`;
|
|
56
|
+
} else {
|
|
57
|
+
html += ` ${key}="${escapeAttr(val)}"`;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Add data-tova-component attribute for named components
|
|
62
|
+
if (vnode && vnode._componentName) {
|
|
63
|
+
html += ` data-tova-component="${escapeAttr(vnode._componentName)}"`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return html;
|
|
67
|
+
}
|
|
68
|
+
|
|
24
69
|
// Render a vnode tree to an HTML string
|
|
25
70
|
export function renderToString(vnode) {
|
|
26
71
|
if (vnode === null || vnode === undefined) {
|
|
@@ -53,36 +98,33 @@ export function renderToString(vnode) {
|
|
|
53
98
|
|
|
54
99
|
// Dynamic node (ErrorBoundary etc.)
|
|
55
100
|
if (vnode.tag === '__dynamic' && typeof vnode.compute === 'function') {
|
|
56
|
-
|
|
101
|
+
const id = nextSSRId();
|
|
102
|
+
try {
|
|
103
|
+
const inner = vnode.compute();
|
|
104
|
+
const content = renderToString(inner);
|
|
105
|
+
return `<!--tova-s:${id}-->${content}<!--/tova-s:${id}-->`;
|
|
106
|
+
} catch (e) {
|
|
107
|
+
// If this is an ErrorBoundary with a fallback, render fallback
|
|
108
|
+
if (vnode._fallback) {
|
|
109
|
+
try {
|
|
110
|
+
const fallbackContent = typeof vnode._fallback === 'function'
|
|
111
|
+
? vnode._fallback({ error: e, reset: () => {} })
|
|
112
|
+
: vnode._fallback;
|
|
113
|
+
return `<!--tova-s:${id}-->${renderToString(fallbackContent)}<!--/tova-s:${id}-->`;
|
|
114
|
+
} catch (fallbackError) {
|
|
115
|
+
// Fallback also threw — re-throw
|
|
116
|
+
throw fallbackError;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
throw e;
|
|
120
|
+
}
|
|
57
121
|
}
|
|
58
122
|
|
|
59
123
|
// Element
|
|
60
124
|
const tag = vnode.tag;
|
|
61
125
|
let html = `<${tag}`;
|
|
62
126
|
|
|
63
|
-
|
|
64
|
-
for (const [key, value] of Object.entries(vnode.props || {})) {
|
|
65
|
-
if (key === 'key' || key === 'ref') continue;
|
|
66
|
-
if (key.startsWith('on')) continue; // skip event handlers in SSR
|
|
67
|
-
|
|
68
|
-
const val = typeof value === 'function' ? value() : value;
|
|
69
|
-
if (val === false || val == null) continue;
|
|
70
|
-
|
|
71
|
-
if (key === 'className') {
|
|
72
|
-
html += ` class="${escapeAttr(val)}"`;
|
|
73
|
-
} else if (key === 'style' && typeof val === 'object') {
|
|
74
|
-
const styleStr = Object.entries(val)
|
|
75
|
-
.map(([k, v]) => `${k.replace(/[A-Z]/g, m => '-' + m.toLowerCase())}:${v}`)
|
|
76
|
-
.join(';');
|
|
77
|
-
html += ` style="${escapeAttr(styleStr)}"`;
|
|
78
|
-
} else if (key === 'checked' || key === 'disabled' || key === 'selected' || key === 'readonly') {
|
|
79
|
-
if (val) html += ` ${key}`;
|
|
80
|
-
} else if (key === 'value') {
|
|
81
|
-
html += ` value="${escapeAttr(val)}"`;
|
|
82
|
-
} else {
|
|
83
|
-
html += ` ${key}="${escapeAttr(val)}"`;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
127
|
+
html += renderPropsToString(vnode.props, vnode);
|
|
86
128
|
|
|
87
129
|
// Self-closing
|
|
88
130
|
if (VOID_ELEMENTS.has(tag)) {
|
|
@@ -132,3 +174,133 @@ export function renderPage(component, { title = 'Tova App', head = '', scriptSrc
|
|
|
132
174
|
</body>
|
|
133
175
|
</html>`;
|
|
134
176
|
}
|
|
177
|
+
|
|
178
|
+
// ─── Streaming SSR ─────────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
// Stream a single vnode, writing chunks to the controller
|
|
181
|
+
function streamVNode(vnode, controller) {
|
|
182
|
+
if (vnode === null || vnode === undefined) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (typeof vnode === 'function') {
|
|
187
|
+
streamVNode(vnode(), controller);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (typeof vnode === 'string') {
|
|
192
|
+
controller.enqueue(escapeHtml(vnode));
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (typeof vnode === 'number' || typeof vnode === 'boolean') {
|
|
197
|
+
controller.enqueue(escapeHtml(String(vnode)));
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (Array.isArray(vnode)) {
|
|
202
|
+
for (const child of vnode) {
|
|
203
|
+
streamVNode(child, controller);
|
|
204
|
+
}
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (!vnode.__tova) {
|
|
209
|
+
controller.enqueue(escapeHtml(String(vnode)));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Fragment
|
|
214
|
+
if (vnode.tag === '__fragment') {
|
|
215
|
+
for (const child of flattenSSR(vnode.children)) {
|
|
216
|
+
streamVNode(child, controller);
|
|
217
|
+
}
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Dynamic node (ErrorBoundary etc.)
|
|
222
|
+
if (vnode.tag === '__dynamic' && typeof vnode.compute === 'function') {
|
|
223
|
+
const id = nextSSRId();
|
|
224
|
+
controller.enqueue(`<!--tova-s:${id}-->`);
|
|
225
|
+
try {
|
|
226
|
+
const inner = vnode.compute();
|
|
227
|
+
streamVNode(inner, controller);
|
|
228
|
+
} catch (e) {
|
|
229
|
+
if (vnode._fallback) {
|
|
230
|
+
try {
|
|
231
|
+
const fallbackContent = typeof vnode._fallback === 'function'
|
|
232
|
+
? vnode._fallback({ error: e, reset: () => {} })
|
|
233
|
+
: vnode._fallback;
|
|
234
|
+
streamVNode(fallbackContent, controller);
|
|
235
|
+
} catch (fallbackError) {
|
|
236
|
+
controller.enqueue(`<!--tova-ssr-error-->`);
|
|
237
|
+
}
|
|
238
|
+
} else {
|
|
239
|
+
// No boundary — re-throw for outer error handling
|
|
240
|
+
controller.enqueue(`<!--/tova-s:${id}-->`);
|
|
241
|
+
throw e;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
controller.enqueue(`<!--/tova-s:${id}-->`);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Element
|
|
249
|
+
const tag = vnode.tag;
|
|
250
|
+
let openTag = `<${tag}`;
|
|
251
|
+
openTag += renderPropsToString(vnode.props, vnode);
|
|
252
|
+
|
|
253
|
+
if (VOID_ELEMENTS.has(tag)) {
|
|
254
|
+
controller.enqueue(openTag + ' />');
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
controller.enqueue(openTag + '>');
|
|
259
|
+
|
|
260
|
+
const children = flattenSSR(vnode.children || []);
|
|
261
|
+
for (const child of children) {
|
|
262
|
+
streamVNode(child, controller);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
controller.enqueue(`</${tag}>`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Render a vnode tree to a Web ReadableStream
|
|
269
|
+
export function renderToReadableStream(vnode, options = {}) {
|
|
270
|
+
const { onError } = options;
|
|
271
|
+
|
|
272
|
+
return new ReadableStream({
|
|
273
|
+
start(controller) {
|
|
274
|
+
try {
|
|
275
|
+
streamVNode(vnode, controller);
|
|
276
|
+
} catch (e) {
|
|
277
|
+
if (onError) onError(e);
|
|
278
|
+
controller.enqueue(`<!--tova-ssr-error-->`);
|
|
279
|
+
}
|
|
280
|
+
controller.close();
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Render a full HTML page as a stream
|
|
286
|
+
export function renderPageToStream(component, options = {}) {
|
|
287
|
+
const { title = 'Tova App', head = '', scriptSrc = '/client.js', onError } = options;
|
|
288
|
+
|
|
289
|
+
return new ReadableStream({
|
|
290
|
+
start(controller) {
|
|
291
|
+
// Flush head immediately so CSS/JS start downloading
|
|
292
|
+
controller.enqueue(`<!DOCTYPE html>\n<html>\n<head>\n <meta charset="utf-8">\n <meta name="viewport" content="width=device-width, initial-scale=1">\n <title>${escapeHtml(title)}</title>\n ${head}\n</head>\n<body>\n <div id="app">`);
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
const vnode = typeof component === 'function' ? component() : component;
|
|
296
|
+
streamVNode(vnode, controller);
|
|
297
|
+
} catch (e) {
|
|
298
|
+
if (onError) onError(e);
|
|
299
|
+
controller.enqueue(`<!--tova-ssr-error-->`);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
controller.enqueue(`</div>\n <script type="module" src="${escapeAttr(scriptSrc)}"></script>\n</body>\n</html>`);
|
|
303
|
+
controller.close();
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
}
|