tova 0.4.5 → 0.4.7
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 +9 -3
- package/package.json +14 -2
- package/src/analyzer/analyzer.js +10 -5
- package/src/analyzer/type-registry.js +22 -3
- package/src/codegen/base-codegen.js +15 -4
- package/src/codegen/client-codegen.js +9 -6
- package/src/codegen/codegen.js +3 -2
- package/src/codegen/server-codegen.js +526 -81
- package/src/lsp/server.js +44 -25
- package/src/parser/server-ast.js +2 -1
- package/src/parser/server-parser.js +12 -1
- package/src/runtime/embedded.js +3 -3
- package/src/runtime/reactivity.js +405 -23
- package/src/runtime/router.js +215 -25
- package/src/runtime/rpc.js +152 -17
- package/src/runtime/ssr.js +66 -10
- package/src/runtime/testing.js +241 -0
- package/src/version.js +1 -1
package/bin/tova.js
CHANGED
|
@@ -1319,13 +1319,19 @@ async function generateDevHTML(clientCode, srcDir, reloadPort = 0) {
|
|
|
1319
1319
|
const liveReloadScript = reloadPort ? `
|
|
1320
1320
|
<script>
|
|
1321
1321
|
(function() {
|
|
1322
|
-
var
|
|
1322
|
+
var reloadUrl = "http://localhost:${reloadPort}/__tova_reload";
|
|
1323
|
+
var es = new EventSource(reloadUrl);
|
|
1324
|
+
var errorCount = 0;
|
|
1325
|
+
es.onopen = function() { errorCount = 0; };
|
|
1323
1326
|
es.onmessage = function(e) { if (e.data === "reload") window.location.reload(); };
|
|
1324
1327
|
es.onerror = function() {
|
|
1328
|
+
errorCount++;
|
|
1329
|
+
// EventSource auto-reconnects — only intervene after repeated failures
|
|
1330
|
+
if (errorCount < 3) return;
|
|
1325
1331
|
es.close();
|
|
1326
|
-
//
|
|
1332
|
+
// SSE server is likely gone (dev server restarting) — poll until back
|
|
1327
1333
|
var check = setInterval(function() {
|
|
1328
|
-
fetch(
|
|
1334
|
+
fetch(reloadUrl, { mode: "no-cors" }).then(function(r) {
|
|
1329
1335
|
clearInterval(check);
|
|
1330
1336
|
window.location.reload();
|
|
1331
1337
|
}).catch(function() {});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tova",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.7",
|
|
4
4
|
"description": "Tova — a modern programming language that transpiles to JavaScript, unifying frontend and backend",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -37,5 +37,17 @@
|
|
|
37
37
|
},
|
|
38
38
|
"author": "Enoch Kujem Abassey",
|
|
39
39
|
"keywords": ["language", "transpiler", "fullstack", "javascript"],
|
|
40
|
-
"license": "MIT"
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@codemirror/autocomplete": "^6.20.0",
|
|
43
|
+
"@codemirror/commands": "^6.10.2",
|
|
44
|
+
"@codemirror/language": "^6.12.1",
|
|
45
|
+
"@codemirror/lint": "^6.9.4",
|
|
46
|
+
"@codemirror/search": "^6.6.0",
|
|
47
|
+
"@codemirror/state": "^6.5.4",
|
|
48
|
+
"@codemirror/theme-one-dark": "^6.1.3",
|
|
49
|
+
"@codemirror/view": "^6.39.15",
|
|
50
|
+
"@lezer/highlight": "^1.2.3",
|
|
51
|
+
"codemirror": "^6.0.2"
|
|
52
|
+
}
|
|
41
53
|
}
|
package/src/analyzer/analyzer.js
CHANGED
|
@@ -2547,11 +2547,13 @@ export class Analyzer {
|
|
|
2547
2547
|
if (typeName) {
|
|
2548
2548
|
const existingImpls = this.typeRegistry.impls.get(typeName) || [];
|
|
2549
2549
|
for (const method of node.methods) {
|
|
2550
|
+
const hasSelf = (method.params || []).some(p => p.name === 'self');
|
|
2550
2551
|
existingImpls.push({
|
|
2551
2552
|
name: method.name,
|
|
2552
2553
|
params: (method.params || []).map(p => p.name),
|
|
2553
2554
|
paramTypes: (method.params || []).map(p => typeAnnotationToType(p.typeAnnotation)),
|
|
2554
2555
|
returnType: typeAnnotationToType(method.returnType),
|
|
2556
|
+
isAssociated: !hasSelf,
|
|
2555
2557
|
});
|
|
2556
2558
|
}
|
|
2557
2559
|
this.typeRegistry.impls.set(typeName, existingImpls);
|
|
@@ -2583,13 +2585,16 @@ export class Analyzer {
|
|
|
2583
2585
|
|
|
2584
2586
|
// Validate that methods reference the type
|
|
2585
2587
|
for (const method of node.methods) {
|
|
2588
|
+
const hasSelf = (method.params || []).some(p => p.name === 'self');
|
|
2586
2589
|
this.pushScope('function');
|
|
2587
2590
|
try {
|
|
2588
|
-
// self is
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2591
|
+
// self is only available for instance methods (not associated functions)
|
|
2592
|
+
if (hasSelf) {
|
|
2593
|
+
try {
|
|
2594
|
+
this.currentScope.define('self',
|
|
2595
|
+
new Symbol('self', 'variable', null, true, method.loc));
|
|
2596
|
+
} catch (e) { /* ignore */ }
|
|
2597
|
+
}
|
|
2593
2598
|
for (const p of method.params) {
|
|
2594
2599
|
if (p.name && p.name !== 'self') {
|
|
2595
2600
|
try {
|
|
@@ -25,7 +25,7 @@ export class TypeRegistry {
|
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
27
|
* Get all members (fields + impl methods) for a type name.
|
|
28
|
-
* Used for dot-completion.
|
|
28
|
+
* Used for dot-completion on instances.
|
|
29
29
|
*/
|
|
30
30
|
getMembers(typeName) {
|
|
31
31
|
const fields = new Map();
|
|
@@ -48,17 +48,36 @@ export class TypeRegistry {
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
// Get
|
|
51
|
+
// Get instance methods (methods with self)
|
|
52
52
|
const implMethods = this.impls.get(typeName);
|
|
53
53
|
if (implMethods) {
|
|
54
54
|
for (const method of implMethods) {
|
|
55
|
-
|
|
55
|
+
if (!method.isAssociated) {
|
|
56
|
+
methods.push(method);
|
|
57
|
+
}
|
|
56
58
|
}
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
return { fields, methods };
|
|
60
62
|
}
|
|
61
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Get associated functions for a type name (functions without self).
|
|
66
|
+
* Used for dot-completion on the type itself (e.g., Point.origin()).
|
|
67
|
+
*/
|
|
68
|
+
getAssociatedFunctions(typeName) {
|
|
69
|
+
const functions = [];
|
|
70
|
+
const implMethods = this.impls.get(typeName);
|
|
71
|
+
if (implMethods) {
|
|
72
|
+
for (const method of implMethods) {
|
|
73
|
+
if (method.isAssociated) {
|
|
74
|
+
functions.push(method);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return functions;
|
|
79
|
+
}
|
|
80
|
+
|
|
62
81
|
/**
|
|
63
82
|
* Get variant names for a type (for match completion).
|
|
64
83
|
*/
|
|
@@ -2898,11 +2898,22 @@ export class BaseCodegen {
|
|
|
2898
2898
|
}
|
|
2899
2899
|
const body = this.genBlockBody(method.body);
|
|
2900
2900
|
this.popScope();
|
|
2901
|
-
|
|
2902
|
-
if (
|
|
2903
|
-
|
|
2901
|
+
|
|
2902
|
+
if (hasSelf) {
|
|
2903
|
+
// Instance method → prototype
|
|
2904
|
+
const selfBinding = `\n${this.i()} const self = this;`;
|
|
2905
|
+
if (hasPropagate) {
|
|
2906
|
+
lines.push(`${this.i()}${node.typeName}.prototype.${method.name} = ${asyncPrefix}function(${paramStr}) {${selfBinding}\n${this.i()} try {\n${body}\n${this.i()} } catch (__e) {\n${this.i()} if (__e && __e.__tova_propagate) return __e.value;\n${this.i()} throw __e;\n${this.i()} }\n${this.i()}};`);
|
|
2907
|
+
} else {
|
|
2908
|
+
lines.push(`${this.i()}${node.typeName}.prototype.${method.name} = ${asyncPrefix}function(${paramStr}) {${selfBinding}\n${body}\n${this.i()}};`);
|
|
2909
|
+
}
|
|
2904
2910
|
} else {
|
|
2905
|
-
|
|
2911
|
+
// Associated function → constructor namespace (callable as Type.method())
|
|
2912
|
+
if (hasPropagate) {
|
|
2913
|
+
lines.push(`${this.i()}${node.typeName}.${method.name} = ${asyncPrefix}function(${paramStr}) {\n${this.i()} try {\n${body}\n${this.i()} } catch (__e) {\n${this.i()} if (__e && __e.__tova_propagate) return __e.value;\n${this.i()} throw __e;\n${this.i()} }\n${this.i()}};`);
|
|
2914
|
+
} else {
|
|
2915
|
+
lines.push(`${this.i()}${node.typeName}.${method.name} = ${asyncPrefix}function(${paramStr}) {\n${body}\n${this.i()}};`);
|
|
2916
|
+
}
|
|
2906
2917
|
}
|
|
2907
2918
|
}
|
|
2908
2919
|
return lines.join('\n');
|
|
@@ -189,8 +189,9 @@ export class ClientCodegen extends BaseCodegen {
|
|
|
189
189
|
const lines = [];
|
|
190
190
|
|
|
191
191
|
// Runtime imports
|
|
192
|
-
lines.push(`import { createSignal, createEffect, createComputed, mount, hydrate, tova_el, tova_fragment, tova_keyed, tova_transition, tova_inject_css, batch, onMount, onUnmount, onCleanup, onBeforeUpdate, createRef, createContext, provide, inject, createErrorBoundary, ErrorBoundary, ErrorInfo, createRoot, watch, untrack, Dynamic, Portal, lazy, Suspense, __tova_action } from './runtime/reactivity.js';`);
|
|
193
|
-
lines.push(`import { rpc } from './runtime/rpc.js';`);
|
|
192
|
+
lines.push(`import { createSignal, createEffect, createComputed, mount, hydrate, tova_el, tova_fragment, tova_keyed, tova_transition, tova_inject_css, batch, onMount, onUnmount, onCleanup, onBeforeUpdate, createRef, createContext, provide, inject, createErrorBoundary, ErrorBoundary, ErrorInfo, createRoot, watch, untrack, Dynamic, Portal, lazy, Suspense, Head, createResource, __tova_action, TransitionGroup, createForm, configureCSP } from './runtime/reactivity.js';`);
|
|
193
|
+
lines.push(`import { rpc, configureRPC, addRPCInterceptor, setCSRFToken } from './runtime/rpc.js';`);
|
|
194
|
+
lines.push(`import { navigate, getCurrentRoute, getParams, getPath, getQuery, defineRoutes, onRouteChange, beforeNavigate, afterNavigate, Router, Outlet, Link, Redirect } from './runtime/router.js';`);
|
|
194
195
|
|
|
195
196
|
// Hoist import lines from shared code to the top of the module
|
|
196
197
|
let sharedRest = sharedCode;
|
|
@@ -394,14 +395,16 @@ export class ClientCodegen extends BaseCodegen {
|
|
|
394
395
|
return p.join('');
|
|
395
396
|
}
|
|
396
397
|
|
|
397
|
-
// Generate a
|
|
398
|
+
// Generate a scope hash from component name + CSS content (for CSS scoping)
|
|
399
|
+
// Uses FNV-1a for better distribution and 8-char output to reduce collision risk.
|
|
398
400
|
_genScopeId(name, css) {
|
|
399
401
|
const str = name + ':' + (css || '');
|
|
400
|
-
let h =
|
|
402
|
+
let h = 0x811c9dc5; // FNV offset basis
|
|
401
403
|
for (let i = 0; i < str.length; i++) {
|
|
402
|
-
h
|
|
404
|
+
h ^= str.charCodeAt(i);
|
|
405
|
+
h = Math.imul(h, 0x01000193); // FNV prime
|
|
403
406
|
}
|
|
404
|
-
return
|
|
407
|
+
return (h >>> 0).toString(36).padStart(8, '0').slice(0, 8);
|
|
405
408
|
}
|
|
406
409
|
|
|
407
410
|
// Scope CSS selectors by appending [data-tova-HASH] to each selector
|
package/src/codegen/codegen.js
CHANGED
|
@@ -279,12 +279,13 @@ export class CodeGenerator {
|
|
|
279
279
|
break;
|
|
280
280
|
}
|
|
281
281
|
case 'ValidateBlock': {
|
|
282
|
-
// Validate: validator function
|
|
282
|
+
// Validate: validator function — include rule expression in error for debuggability
|
|
283
283
|
const rules = stmt.rules.map(r => gen.genExpression(r));
|
|
284
284
|
lines.push(`function __validate_${stmt.typeName}(it) {`);
|
|
285
285
|
lines.push(` const errors = [];`);
|
|
286
286
|
for (let i = 0; i < rules.length; i++) {
|
|
287
|
-
|
|
287
|
+
const escapedRule = rules[i].replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
288
|
+
lines.push(` if (!(${rules[i]})) errors.push("Validation failed for ${stmt.typeName}: expected \`${escapedRule}\`");`);
|
|
288
289
|
}
|
|
289
290
|
lines.push(` return errors.length === 0 ? { valid: true, errors: [] } : { valid: false, errors };`);
|
|
290
291
|
lines.push(`}`);
|