tova 0.7.0 → 0.9.4
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 +1312 -139
- package/package.json +8 -1
- package/src/analyzer/analyzer.js +539 -11
- package/src/analyzer/browser-analyzer.js +56 -8
- package/src/analyzer/deploy-analyzer.js +44 -0
- package/src/analyzer/scope.js +7 -0
- package/src/analyzer/server-analyzer.js +33 -1
- package/src/codegen/base-codegen.js +1296 -23
- package/src/codegen/browser-codegen.js +725 -20
- package/src/codegen/codegen.js +87 -5
- package/src/codegen/deploy-codegen.js +49 -0
- package/src/codegen/server-codegen.js +54 -6
- package/src/codegen/shared-codegen.js +5 -0
- package/src/codegen/theme-codegen.js +69 -0
- package/src/codegen/wasm-codegen.js +6 -0
- package/src/config/edit-toml.js +6 -2
- package/src/config/git-resolver.js +128 -0
- package/src/config/lock-file.js +57 -0
- package/src/config/module-cache.js +58 -0
- package/src/config/module-entry.js +37 -0
- package/src/config/module-path.js +63 -0
- package/src/config/pkg-errors.js +62 -0
- package/src/config/resolve.js +26 -0
- package/src/config/resolver.js +139 -0
- package/src/config/search.js +28 -0
- package/src/config/semver.js +72 -0
- package/src/config/toml.js +61 -6
- package/src/deploy/deploy.js +217 -0
- package/src/deploy/infer.js +218 -0
- package/src/deploy/provision.js +315 -0
- package/src/diagnostics/security-scorecard.js +111 -0
- package/src/lexer/lexer.js +18 -3
- package/src/lsp/server.js +482 -0
- package/src/parser/animate-ast.js +45 -0
- package/src/parser/ast.js +39 -0
- package/src/parser/browser-ast.js +19 -1
- package/src/parser/browser-parser.js +221 -4
- package/src/parser/concurrency-ast.js +15 -0
- package/src/parser/concurrency-parser.js +236 -0
- package/src/parser/deploy-ast.js +37 -0
- package/src/parser/deploy-parser.js +132 -0
- package/src/parser/parser.js +42 -5
- package/src/parser/select-ast.js +39 -0
- package/src/parser/theme-ast.js +29 -0
- package/src/parser/theme-parser.js +70 -0
- package/src/registry/plugins/concurrency-plugin.js +32 -0
- package/src/registry/plugins/deploy-plugin.js +33 -0
- package/src/registry/plugins/theme-plugin.js +20 -0
- package/src/registry/register-all.js +6 -0
- package/src/runtime/charts.js +547 -0
- package/src/runtime/embedded.js +6 -2
- package/src/runtime/reactivity.js +60 -0
- package/src/runtime/router.js +703 -295
- package/src/runtime/table.js +606 -33
- package/src/stdlib/inline.js +365 -10
- package/src/stdlib/runtime-bridge.js +152 -0
- package/src/stdlib/string.js +84 -2
- package/src/stdlib/validation.js +1 -1
- package/src/version.js +1 -1
|
@@ -12,7 +12,23 @@ export function installBrowserAnalyzer(AnalyzerClass) {
|
|
|
12
12
|
|
|
13
13
|
AnalyzerClass.prototype.visitBrowserBlock = function(node) {
|
|
14
14
|
const prevScope = this.currentScope;
|
|
15
|
-
|
|
15
|
+
let browserScope = null;
|
|
16
|
+
for (const ch of this.currentScope.children) {
|
|
17
|
+
if (ch.context === 'browser') { browserScope = ch; break; }
|
|
18
|
+
}
|
|
19
|
+
const isFirst = !browserScope;
|
|
20
|
+
this.currentScope = browserScope || this.currentScope.child('browser');
|
|
21
|
+
|
|
22
|
+
// On first browser block, pre-register state/computed/function names from ALL
|
|
23
|
+
// browser blocks so cross-file references resolve regardless of file order.
|
|
24
|
+
if (isFirst && this.ast && this.ast.body) {
|
|
25
|
+
for (const topNode of this.ast.body) {
|
|
26
|
+
if (topNode.type === 'BrowserBlock') {
|
|
27
|
+
this._preRegisterBrowserDecls(topNode.body);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
16
32
|
try {
|
|
17
33
|
for (const stmt of node.body) {
|
|
18
34
|
this.visitNode(stmt);
|
|
@@ -22,9 +38,26 @@ export function installBrowserAnalyzer(AnalyzerClass) {
|
|
|
22
38
|
}
|
|
23
39
|
};
|
|
24
40
|
|
|
41
|
+
AnalyzerClass.prototype._preRegisterBrowserDecls = function(stmts) {
|
|
42
|
+
for (const stmt of stmts) {
|
|
43
|
+
const name = stmt.name;
|
|
44
|
+
if (!name) continue;
|
|
45
|
+
let kind = null;
|
|
46
|
+
if (stmt.type === 'StateDeclaration') kind = 'state';
|
|
47
|
+
else if (stmt.type === 'ComputedDeclaration') kind = 'computed';
|
|
48
|
+
else if (stmt.type === 'FunctionDeclaration') kind = 'function';
|
|
49
|
+
else if (stmt.type === 'ComponentDeclaration') kind = 'component';
|
|
50
|
+
if (kind && !this.currentScope.symbols.has(name)) {
|
|
51
|
+
const sym = new Symbol(name, kind, stmt.typeAnnotation || null, kind === 'state', stmt.loc);
|
|
52
|
+
sym._forward = true;
|
|
53
|
+
try { this.currentScope.define(name, sym); } catch (e) { /* ignore */ }
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
25
58
|
AnalyzerClass.prototype.visitStateDeclaration = function(node) {
|
|
26
59
|
const ctx = this.currentScope.getContext();
|
|
27
|
-
if (ctx !== 'browser') {
|
|
60
|
+
if (ctx !== 'browser' && !this._inPubComponent) {
|
|
28
61
|
this.error(`'state' can only be used inside a browser block`, node.loc, "move this inside a browser { } block", { code: 'E302' });
|
|
29
62
|
}
|
|
30
63
|
try {
|
|
@@ -38,7 +71,7 @@ export function installBrowserAnalyzer(AnalyzerClass) {
|
|
|
38
71
|
|
|
39
72
|
AnalyzerClass.prototype.visitComputedDeclaration = function(node) {
|
|
40
73
|
const ctx = this.currentScope.getContext();
|
|
41
|
-
if (ctx !== 'browser') {
|
|
74
|
+
if (ctx !== 'browser' && !this._inPubComponent) {
|
|
42
75
|
this.error(`'computed' can only be used inside a browser block`, node.loc, "move this inside a browser { } block", { code: 'E302' });
|
|
43
76
|
}
|
|
44
77
|
try {
|
|
@@ -52,7 +85,7 @@ export function installBrowserAnalyzer(AnalyzerClass) {
|
|
|
52
85
|
|
|
53
86
|
AnalyzerClass.prototype.visitEffectDeclaration = function(node) {
|
|
54
87
|
const ctx = this.currentScope.getContext();
|
|
55
|
-
if (ctx !== 'browser') {
|
|
88
|
+
if (ctx !== 'browser' && !this._inPubComponent) {
|
|
56
89
|
this.error(`'effect' can only be used inside a browser block`, node.loc, "move this inside a browser { } block", { code: 'E302' });
|
|
57
90
|
}
|
|
58
91
|
this.visitNode(node.body);
|
|
@@ -60,19 +93,32 @@ export function installBrowserAnalyzer(AnalyzerClass) {
|
|
|
60
93
|
|
|
61
94
|
AnalyzerClass.prototype.visitComponentDeclaration = function(node) {
|
|
62
95
|
const ctx = this.currentScope.getContext();
|
|
63
|
-
if (ctx !== 'browser') {
|
|
96
|
+
if (ctx !== 'browser' && !node.isPublic) {
|
|
64
97
|
this.error(`'component' can only be used inside a browser block`, node.loc, "move this inside a browser { } block", { code: 'E302' });
|
|
65
98
|
}
|
|
66
|
-
|
|
99
|
+
// Skip naming convention check for compound components (e.g. Dialog.Title)
|
|
100
|
+
// since "Dialog.Title" isn't a single PascalCase identifier
|
|
101
|
+
if (!node.parent) {
|
|
102
|
+
this._checkNamingConvention(node.name, 'component', node.loc);
|
|
103
|
+
}
|
|
104
|
+
// For compound components, register with the parent name (already defined)
|
|
105
|
+
// and use the full name for the symbol definition
|
|
106
|
+
const symbolName = node.name;
|
|
67
107
|
try {
|
|
68
|
-
this.currentScope.define(
|
|
69
|
-
new Symbol(
|
|
108
|
+
this.currentScope.define(symbolName,
|
|
109
|
+
new Symbol(symbolName, 'component', null, false, node.loc));
|
|
70
110
|
} catch (e) {
|
|
71
111
|
this.error(e.message);
|
|
72
112
|
}
|
|
73
113
|
|
|
74
114
|
const prevScope = this.currentScope;
|
|
75
115
|
this.currentScope = this.currentScope.child('function');
|
|
116
|
+
// Store component prop names for variant() validation in style blocks
|
|
117
|
+
const prevComponentProps = this._currentComponentProps;
|
|
118
|
+
this._currentComponentProps = node.params.map(p => p.name);
|
|
119
|
+
// Track pub component context so state/computed/effect are allowed inside
|
|
120
|
+
const prevInPubComponent = this._inPubComponent;
|
|
121
|
+
if (node.isPublic) this._inPubComponent = true;
|
|
76
122
|
for (const param of node.params) {
|
|
77
123
|
try {
|
|
78
124
|
this.currentScope.define(param.name,
|
|
@@ -86,6 +132,8 @@ export function installBrowserAnalyzer(AnalyzerClass) {
|
|
|
86
132
|
this.visitNode(child);
|
|
87
133
|
}
|
|
88
134
|
} finally {
|
|
135
|
+
this._currentComponentProps = prevComponentProps;
|
|
136
|
+
this._inPubComponent = prevInPubComponent;
|
|
89
137
|
this.currentScope = prevScope;
|
|
90
138
|
}
|
|
91
139
|
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Deploy-specific analyzer methods for the Tova language
|
|
2
|
+
// Extracted from analyzer.js for lazy loading — only loaded when deploy { } blocks are encountered.
|
|
3
|
+
|
|
4
|
+
const KNOWN_DEPLOY_FIELDS = new Set([
|
|
5
|
+
'server', 'domain', 'instances', 'memory', 'branch',
|
|
6
|
+
'health', 'health_interval', 'health_timeout',
|
|
7
|
+
'restart_on_failure', 'keep_releases',
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
const REQUIRED_DEPLOY_FIELDS = ['server', 'domain'];
|
|
11
|
+
|
|
12
|
+
export function installDeployAnalyzer(AnalyzerClass) {
|
|
13
|
+
if (AnalyzerClass.prototype._deployAnalyzerInstalled) return;
|
|
14
|
+
AnalyzerClass.prototype._deployAnalyzerInstalled = true;
|
|
15
|
+
|
|
16
|
+
AnalyzerClass.prototype.visitDeployBlock = function(node) {
|
|
17
|
+
// Collect config field keys present in the deploy block body
|
|
18
|
+
const presentFields = new Set();
|
|
19
|
+
for (const stmt of node.body) {
|
|
20
|
+
if (stmt.type === 'DeployConfigField') {
|
|
21
|
+
// Validate unknown fields
|
|
22
|
+
if (!KNOWN_DEPLOY_FIELDS.has(stmt.key)) {
|
|
23
|
+
this.error(
|
|
24
|
+
`Unknown deploy config field "${stmt.key}"`,
|
|
25
|
+
stmt.loc,
|
|
26
|
+
`Known fields: ${[...KNOWN_DEPLOY_FIELDS].join(', ')}`
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
presentFields.add(stmt.key);
|
|
30
|
+
}
|
|
31
|
+
// DeployEnvBlock and DeployDbBlock are valid sub-blocks — no additional validation needed
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Validate required fields
|
|
35
|
+
for (const required of REQUIRED_DEPLOY_FIELDS) {
|
|
36
|
+
if (!presentFields.has(required)) {
|
|
37
|
+
this.error(
|
|
38
|
+
`Deploy block "${node.name}" is missing required field "${required}"`,
|
|
39
|
+
node.loc
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
package/src/analyzer/scope.js
CHANGED
|
@@ -28,6 +28,13 @@ export class Scope {
|
|
|
28
28
|
const existing = this.symbols.get(name);
|
|
29
29
|
// Allow user code to shadow builtins
|
|
30
30
|
if (existing.kind === 'builtin') {
|
|
31
|
+
if (existing.used) symbol.used = true;
|
|
32
|
+
this.symbols.set(name, symbol);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
// Allow real definitions to overwrite forward-declared symbols
|
|
36
|
+
if (existing._forward) {
|
|
37
|
+
if (existing.used) symbol.used = true;
|
|
31
38
|
this.symbols.set(name, symbol);
|
|
32
39
|
return;
|
|
33
40
|
}
|
|
@@ -37,7 +37,22 @@ export function installServerAnalyzer(AnalyzerClass) {
|
|
|
37
37
|
const prevScope = this.currentScope;
|
|
38
38
|
const prevServerBlockName = this._currentServerBlockName;
|
|
39
39
|
this._currentServerBlockName = node.name || null;
|
|
40
|
-
|
|
40
|
+
let serverScope = null;
|
|
41
|
+
for (const ch of this.currentScope.children) {
|
|
42
|
+
if (ch.context === 'server') { serverScope = ch; break; }
|
|
43
|
+
}
|
|
44
|
+
const isFirst = !serverScope;
|
|
45
|
+
this.currentScope = serverScope || this.currentScope.child('server');
|
|
46
|
+
|
|
47
|
+
// On first server block, pre-register function/var/type names from ALL
|
|
48
|
+
// server blocks so cross-file references resolve regardless of file order.
|
|
49
|
+
if (isFirst && this.ast && this.ast.body) {
|
|
50
|
+
for (const topNode of this.ast.body) {
|
|
51
|
+
if (topNode.type === 'ServerBlock') {
|
|
52
|
+
this._preRegisterServerDecls(topNode.body);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
41
56
|
|
|
42
57
|
try {
|
|
43
58
|
// Register peer server block names as valid identifiers in this scope
|
|
@@ -76,6 +91,23 @@ export function installServerAnalyzer(AnalyzerClass) {
|
|
|
76
91
|
}
|
|
77
92
|
};
|
|
78
93
|
|
|
94
|
+
AnalyzerClass.prototype._preRegisterServerDecls = function(stmts) {
|
|
95
|
+
for (const stmt of stmts) {
|
|
96
|
+
const name = stmt.name;
|
|
97
|
+
if (!name) continue;
|
|
98
|
+
let kind = null;
|
|
99
|
+
if (stmt.type === 'FunctionDeclaration') kind = 'function';
|
|
100
|
+
else if (stmt.type === 'VarDeclaration') kind = 'variable';
|
|
101
|
+
else if (stmt.type === 'TypeDeclaration') kind = 'type';
|
|
102
|
+
else if (stmt.type === 'ModelDeclaration') kind = 'type';
|
|
103
|
+
if (kind && !this.currentScope.symbols.has(name)) {
|
|
104
|
+
const sym = new Symbol(name, kind, stmt.typeAnnotation || null, kind === 'variable', stmt.loc);
|
|
105
|
+
sym._forward = true;
|
|
106
|
+
try { this.currentScope.define(name, sym); } catch (e) { /* ignore */ }
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
79
111
|
AnalyzerClass.prototype.visitRouteDeclaration = function(node) {
|
|
80
112
|
const ctx = this.currentScope.getContext();
|
|
81
113
|
if (ctx !== 'server') {
|