tova 0.3.4 → 0.3.6
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/tova.js +438 -58
- package/package.json +1 -1
- package/src/analyzer/analyzer.js +172 -32
- package/src/analyzer/client-analyzer.js +21 -5
- package/src/analyzer/scope.js +78 -3
- package/src/codegen/base-codegen.js +754 -45
- package/src/codegen/client-codegen.js +293 -36
- package/src/codegen/codegen.js +10 -15
- package/src/codegen/server-codegen.js +189 -40
- package/src/codegen/wasm-codegen.js +610 -0
- package/src/lexer/lexer.js +157 -109
- package/src/lexer/tokens.js +3 -0
- package/src/lsp/server.js +148 -12
- package/src/parser/ast.js +2 -1
- package/src/parser/client-parser.js +10 -3
- package/src/parser/parser.js +144 -150
- package/src/runtime/embedded.js +1 -1
- package/src/runtime/reactivity.js +307 -59
- package/src/runtime/ssr.js +101 -34
- package/src/stdlib/inline.js +333 -24
- package/src/stdlib/native-bridge.js +150 -0
- package/src/version.js +1 -1
package/src/runtime/ssr.js
CHANGED
|
@@ -7,18 +7,24 @@ const VOID_ELEMENTS = new Set([
|
|
|
7
7
|
'link', 'meta', 'param', 'source', 'track', 'wbr',
|
|
8
8
|
]);
|
|
9
9
|
|
|
10
|
+
const _ESC_HTML = { '&': '&', '<': '<', '>': '>', '"': '"' };
|
|
11
|
+
const _RE_HTML = /[&<>"]/;
|
|
12
|
+
const _RE_HTML_G = /[&<>"]/g;
|
|
13
|
+
|
|
10
14
|
function escapeHtml(str) {
|
|
11
15
|
if (typeof str !== 'string') return String(str);
|
|
12
|
-
return str
|
|
13
|
-
|
|
14
|
-
.replace(/</g, '<')
|
|
15
|
-
.replace(/>/g, '>')
|
|
16
|
-
.replace(/"/g, '"');
|
|
16
|
+
if (!_RE_HTML.test(str)) return str; // fast path: no special chars
|
|
17
|
+
return str.replace(_RE_HTML_G, ch => _ESC_HTML[ch]);
|
|
17
18
|
}
|
|
18
19
|
|
|
20
|
+
const _ESC_ATTR = { '&': '&', '"': '"' };
|
|
21
|
+
const _RE_ATTR = /[&"]/;
|
|
22
|
+
const _RE_ATTR_G = /[&"]/g;
|
|
23
|
+
|
|
19
24
|
function escapeAttr(str) {
|
|
20
25
|
if (typeof str !== 'string') return String(str);
|
|
21
|
-
|
|
26
|
+
if (!_RE_ATTR.test(str)) return str; // fast path
|
|
27
|
+
return str.replace(_RE_ATTR_G, ch => _ESC_ATTR[ch]);
|
|
22
28
|
}
|
|
23
29
|
|
|
24
30
|
// ─── SSR ID Counter for hydration markers ─────────────────
|
|
@@ -68,41 +74,68 @@ function renderPropsToString(props, vnode) {
|
|
|
68
74
|
|
|
69
75
|
// Render a vnode tree to an HTML string
|
|
70
76
|
export function renderToString(vnode) {
|
|
77
|
+
const parts = [];
|
|
78
|
+
_renderParts(vnode, parts);
|
|
79
|
+
return parts.join('');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function _renderParts(vnode, parts) {
|
|
71
83
|
if (vnode === null || vnode === undefined) {
|
|
72
|
-
return
|
|
84
|
+
return;
|
|
73
85
|
}
|
|
74
86
|
|
|
75
87
|
// Reactive function — evaluate it
|
|
76
88
|
if (typeof vnode === 'function') {
|
|
77
|
-
|
|
89
|
+
_renderParts(vnode(), parts);
|
|
90
|
+
return;
|
|
78
91
|
}
|
|
79
92
|
|
|
80
93
|
// Primitives
|
|
81
|
-
if (typeof vnode === 'string')
|
|
82
|
-
if (typeof vnode === 'number' || typeof vnode === 'boolean')
|
|
94
|
+
if (typeof vnode === 'string') { parts.push(escapeHtml(vnode)); return; }
|
|
95
|
+
if (typeof vnode === 'number' || typeof vnode === 'boolean') { parts.push(escapeHtml(String(vnode))); return; }
|
|
83
96
|
|
|
84
97
|
// Arrays
|
|
85
98
|
if (Array.isArray(vnode)) {
|
|
86
|
-
|
|
99
|
+
for (const child of vnode) _renderParts(child, parts);
|
|
100
|
+
return;
|
|
87
101
|
}
|
|
88
102
|
|
|
89
103
|
// Non-tova object
|
|
90
104
|
if (!vnode.__tova) {
|
|
91
|
-
|
|
105
|
+
parts.push(escapeHtml(String(vnode)));
|
|
106
|
+
return;
|
|
92
107
|
}
|
|
93
108
|
|
|
94
109
|
// Fragment
|
|
95
110
|
if (vnode.tag === '__fragment') {
|
|
96
|
-
|
|
111
|
+
const children = flattenSSR(vnode.children);
|
|
112
|
+
for (const child of children) _renderParts(child, parts);
|
|
113
|
+
return;
|
|
97
114
|
}
|
|
98
115
|
|
|
99
|
-
// Dynamic node (ErrorBoundary etc.)
|
|
116
|
+
// Dynamic node (ErrorBoundary, Suspense, etc.)
|
|
100
117
|
if (vnode.tag === '__dynamic' && typeof vnode.compute === 'function') {
|
|
101
118
|
const id = nextSSRId();
|
|
102
119
|
try {
|
|
103
120
|
const inner = vnode.compute();
|
|
104
|
-
|
|
105
|
-
|
|
121
|
+
// If inner is a Promise (async Suspense children), render fallback
|
|
122
|
+
if (inner && typeof inner.then === 'function') {
|
|
123
|
+
if (vnode._fallback) {
|
|
124
|
+
const fallbackContent = typeof vnode._fallback === 'function'
|
|
125
|
+
? vnode._fallback()
|
|
126
|
+
: vnode._fallback;
|
|
127
|
+
parts.push(`<!--tova-s:${id}-->`);
|
|
128
|
+
_renderParts(fallbackContent, parts);
|
|
129
|
+
parts.push(`<!--/tova-s:${id}-->`);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
parts.push(`<!--tova-s:${id}--><!--/tova-s:${id}-->`);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
parts.push(`<!--tova-s:${id}-->`);
|
|
136
|
+
_renderParts(inner, parts);
|
|
137
|
+
parts.push(`<!--/tova-s:${id}-->`);
|
|
138
|
+
return;
|
|
106
139
|
} catch (e) {
|
|
107
140
|
// If this is an ErrorBoundary with a fallback, render fallback
|
|
108
141
|
if (vnode._fallback) {
|
|
@@ -110,9 +143,11 @@ export function renderToString(vnode) {
|
|
|
110
143
|
const fallbackContent = typeof vnode._fallback === 'function'
|
|
111
144
|
? vnode._fallback({ error: e, reset: () => {} })
|
|
112
145
|
: vnode._fallback;
|
|
113
|
-
|
|
146
|
+
parts.push(`<!--tova-s:${id}-->`);
|
|
147
|
+
_renderParts(fallbackContent, parts);
|
|
148
|
+
parts.push(`<!--/tova-s:${id}-->`);
|
|
149
|
+
return;
|
|
114
150
|
} catch (fallbackError) {
|
|
115
|
-
// Fallback also threw — re-throw
|
|
116
151
|
throw fallbackError;
|
|
117
152
|
}
|
|
118
153
|
}
|
|
@@ -122,25 +157,24 @@ export function renderToString(vnode) {
|
|
|
122
157
|
|
|
123
158
|
// Element
|
|
124
159
|
const tag = vnode.tag;
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
html += renderPropsToString(vnode.props, vnode);
|
|
160
|
+
parts.push(`<${tag}`);
|
|
161
|
+
parts.push(renderPropsToString(vnode.props, vnode));
|
|
128
162
|
|
|
129
163
|
// Self-closing
|
|
130
164
|
if (VOID_ELEMENTS.has(tag)) {
|
|
131
|
-
|
|
165
|
+
parts.push(' />');
|
|
166
|
+
return;
|
|
132
167
|
}
|
|
133
168
|
|
|
134
|
-
|
|
169
|
+
parts.push('>');
|
|
135
170
|
|
|
136
171
|
// Children
|
|
137
172
|
const children = flattenSSR(vnode.children || []);
|
|
138
173
|
for (const child of children) {
|
|
139
|
-
|
|
174
|
+
_renderParts(child, parts);
|
|
140
175
|
}
|
|
141
176
|
|
|
142
|
-
|
|
143
|
-
return html;
|
|
177
|
+
parts.push(`</${tag}>`);
|
|
144
178
|
}
|
|
145
179
|
|
|
146
180
|
function flattenSSR(children) {
|
|
@@ -177,6 +211,36 @@ export function renderPage(component, { title = 'Tova App', head = '', scriptSrc
|
|
|
177
211
|
|
|
178
212
|
// ─── Streaming SSR ─────────────────────────────────────────
|
|
179
213
|
|
|
214
|
+
// Buffered controller wrapper — batches small enqueue() calls into larger chunks
|
|
215
|
+
// to reduce the number of stream operations (N4 optimization)
|
|
216
|
+
class BufferedController {
|
|
217
|
+
constructor(controller, bufferSize = 4096) {
|
|
218
|
+
this._inner = controller;
|
|
219
|
+
this._buffer = '';
|
|
220
|
+
this._bufferSize = bufferSize;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
enqueue(chunk) {
|
|
224
|
+
this._buffer += chunk;
|
|
225
|
+
if (this._buffer.length >= this._bufferSize) {
|
|
226
|
+
this._inner.enqueue(this._buffer);
|
|
227
|
+
this._buffer = '';
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
flush() {
|
|
232
|
+
if (this._buffer.length > 0) {
|
|
233
|
+
this._inner.enqueue(this._buffer);
|
|
234
|
+
this._buffer = '';
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
close() {
|
|
239
|
+
this.flush();
|
|
240
|
+
this._inner.close();
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
180
244
|
// Stream a single vnode, writing chunks to the controller
|
|
181
245
|
function streamVNode(vnode, controller) {
|
|
182
246
|
if (vnode === null || vnode === undefined) {
|
|
@@ -267,38 +331,41 @@ function streamVNode(vnode, controller) {
|
|
|
267
331
|
|
|
268
332
|
// Render a vnode tree to a Web ReadableStream
|
|
269
333
|
export function renderToReadableStream(vnode, options = {}) {
|
|
270
|
-
const { onError } = options;
|
|
334
|
+
const { onError, bufferSize } = options;
|
|
271
335
|
|
|
272
336
|
return new ReadableStream({
|
|
273
337
|
start(controller) {
|
|
338
|
+
const buf = new BufferedController(controller, bufferSize);
|
|
274
339
|
try {
|
|
275
|
-
streamVNode(vnode,
|
|
340
|
+
streamVNode(vnode, buf);
|
|
276
341
|
} catch (e) {
|
|
277
342
|
if (onError) onError(e);
|
|
278
|
-
|
|
343
|
+
buf.enqueue(`<!--tova-ssr-error-->`);
|
|
279
344
|
}
|
|
280
|
-
|
|
345
|
+
buf.close();
|
|
281
346
|
},
|
|
282
347
|
});
|
|
283
348
|
}
|
|
284
349
|
|
|
285
350
|
// Render a full HTML page as a stream
|
|
286
351
|
export function renderPageToStream(component, options = {}) {
|
|
287
|
-
const { title = 'Tova App', head = '', scriptSrc = '/client.js', onError } = options;
|
|
352
|
+
const { title = 'Tova App', head = '', scriptSrc = '/client.js', onError, bufferSize } = options;
|
|
288
353
|
|
|
289
354
|
return new ReadableStream({
|
|
290
355
|
start(controller) {
|
|
291
|
-
// Flush head immediately so CSS/JS start downloading
|
|
356
|
+
// Flush head immediately so CSS/JS start downloading (bypass buffer)
|
|
292
357
|
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
358
|
|
|
359
|
+
const buf = new BufferedController(controller, bufferSize);
|
|
294
360
|
try {
|
|
295
361
|
const vnode = typeof component === 'function' ? component() : component;
|
|
296
|
-
streamVNode(vnode,
|
|
362
|
+
streamVNode(vnode, buf);
|
|
297
363
|
} catch (e) {
|
|
298
364
|
if (onError) onError(e);
|
|
299
|
-
|
|
365
|
+
buf.enqueue(`<!--tova-ssr-error-->`);
|
|
300
366
|
}
|
|
301
367
|
|
|
368
|
+
buf.flush();
|
|
302
369
|
controller.enqueue(`</div>\n <script type="module" src="${escapeAttr(scriptSrc)}"></script>\n</body>\n</html>`);
|
|
303
370
|
controller.close();
|
|
304
371
|
},
|