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
package/src/parser/parser.js
CHANGED
|
@@ -131,13 +131,15 @@ export class Parser {
|
|
|
131
131
|
_isContextualKeyword() {
|
|
132
132
|
const t = this.current().type;
|
|
133
133
|
return t === TokenType.ROUTE || t === TokenType.STATE || t === TokenType.COMPUTED ||
|
|
134
|
-
t === TokenType.EFFECT || t === TokenType.COMPONENT || t === TokenType.STORE
|
|
134
|
+
t === TokenType.EFFECT || t === TokenType.COMPONENT || t === TokenType.STORE ||
|
|
135
|
+
t === TokenType.FORM || t === TokenType.FIELD || t === TokenType.GROUP || t === TokenType.STEPS;
|
|
135
136
|
}
|
|
136
137
|
|
|
137
138
|
_isContextualKeywordToken(token) {
|
|
138
139
|
const t = token.type;
|
|
139
140
|
return t === TokenType.ROUTE || t === TokenType.STATE || t === TokenType.COMPUTED ||
|
|
140
|
-
t === TokenType.EFFECT || t === TokenType.COMPONENT || t === TokenType.STORE
|
|
141
|
+
t === TokenType.EFFECT || t === TokenType.COMPONENT || t === TokenType.STORE ||
|
|
142
|
+
t === TokenType.FORM || t === TokenType.FIELD || t === TokenType.GROUP || t === TokenType.STEPS;
|
|
141
143
|
}
|
|
142
144
|
|
|
143
145
|
_synchronizeBlock() {
|
|
@@ -210,6 +212,13 @@ export class Parser {
|
|
|
210
212
|
// ─── Program ───────────────────────────────────────────────
|
|
211
213
|
|
|
212
214
|
parse() {
|
|
215
|
+
// Eagerly install all block-plugin parser extensions so they work inside function bodies
|
|
216
|
+
for (const plugin of BlockRegistry.all()) {
|
|
217
|
+
const p = plugin.parser;
|
|
218
|
+
if (p.install && p.installedFlag && !Parser.prototype[p.installedFlag]) {
|
|
219
|
+
p.install(Parser);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
213
222
|
const body = [];
|
|
214
223
|
const maxErrors = 50; // Stop after 50 errors to avoid cascading noise
|
|
215
224
|
while (!this.isAtEnd()) {
|
|
@@ -590,6 +599,12 @@ export class Parser {
|
|
|
590
599
|
if (this.check(TokenType.PUB)) {
|
|
591
600
|
this.error("Duplicate 'pub' modifier");
|
|
592
601
|
}
|
|
602
|
+
// Handle pub component at top level (parseComponent is installed by browser-parser plugin)
|
|
603
|
+
if (this.check(TokenType.COMPONENT) && typeof this.parseComponent === 'function') {
|
|
604
|
+
const comp = this.parseComponent();
|
|
605
|
+
comp.isPublic = true;
|
|
606
|
+
return comp;
|
|
607
|
+
}
|
|
593
608
|
const stmt = this.parseStatement();
|
|
594
609
|
if (stmt) stmt.isPublic = true;
|
|
595
610
|
return stmt;
|
|
@@ -1218,7 +1233,12 @@ export class Parser {
|
|
|
1218
1233
|
const elements = [];
|
|
1219
1234
|
|
|
1220
1235
|
while (!this.check(TokenType.RBRACKET) && !this.isAtEnd()) {
|
|
1221
|
-
if (this.check(TokenType.
|
|
1236
|
+
if (this.check(TokenType.SPREAD)) {
|
|
1237
|
+
this.advance(); // consume ...
|
|
1238
|
+
const restName = this.expect(TokenType.IDENTIFIER, "Expected identifier after '...'").value;
|
|
1239
|
+
elements.push('...' + restName);
|
|
1240
|
+
break; // rest must be last
|
|
1241
|
+
} else if (this.check(TokenType.IDENTIFIER) && this.current().value === '_') {
|
|
1222
1242
|
elements.push(null); // skip placeholder
|
|
1223
1243
|
this.advance();
|
|
1224
1244
|
} else {
|
|
@@ -1476,7 +1496,14 @@ export class Parser {
|
|
|
1476
1496
|
// Destructuring without let: {name, age} = user or [a, b] = list
|
|
1477
1497
|
if (expr.type === 'ObjectLiteral') {
|
|
1478
1498
|
const pattern = new AST.ObjectPattern(
|
|
1479
|
-
expr.properties.map(p =>
|
|
1499
|
+
expr.properties.map(p => {
|
|
1500
|
+
const key = typeof p.key === 'string' ? p.key : p.key.name || p.key;
|
|
1501
|
+
// For shorthand {name}, key and value are the same
|
|
1502
|
+
// For rename {name: alias}, value is the alias identifier
|
|
1503
|
+
const val = p.shorthand ? key
|
|
1504
|
+
: (p.value && p.value.type === 'Identifier' ? p.value.name : key);
|
|
1505
|
+
return { key, value: val };
|
|
1506
|
+
}),
|
|
1480
1507
|
expr.loc
|
|
1481
1508
|
);
|
|
1482
1509
|
const value = this.parseExpression();
|
|
@@ -1484,7 +1511,13 @@ export class Parser {
|
|
|
1484
1511
|
}
|
|
1485
1512
|
if (expr.type === 'ArrayLiteral') {
|
|
1486
1513
|
const pattern = new AST.ArrayPattern(
|
|
1487
|
-
expr.elements.map(e =>
|
|
1514
|
+
expr.elements.map(e => {
|
|
1515
|
+
if (e.type === 'Identifier') return e.name;
|
|
1516
|
+
if (e.type === 'SpreadExpression' && e.argument && e.argument.type === 'Identifier') {
|
|
1517
|
+
return '...' + e.argument.name;
|
|
1518
|
+
}
|
|
1519
|
+
return '_';
|
|
1520
|
+
}),
|
|
1488
1521
|
expr.loc
|
|
1489
1522
|
);
|
|
1490
1523
|
const value = this.parseExpression();
|
|
@@ -2003,6 +2036,10 @@ export class Parser {
|
|
|
2003
2036
|
case TokenType.BROWSER:
|
|
2004
2037
|
case TokenType.SHARED:
|
|
2005
2038
|
case TokenType.DERIVE:
|
|
2039
|
+
case TokenType.FORM:
|
|
2040
|
+
case TokenType.FIELD:
|
|
2041
|
+
case TokenType.GROUP:
|
|
2042
|
+
case TokenType.STEPS:
|
|
2006
2043
|
return new AST.Identifier(this.advance().value, l);
|
|
2007
2044
|
|
|
2008
2045
|
case TokenType.IDENTIFIER: {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Select-specific AST Node definitions for the Tova language
|
|
2
|
+
// Extracted for lazy loading -- only loaded when select { } blocks are used.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* select {
|
|
6
|
+
* msg from ch => { ... }
|
|
7
|
+
* ch.send(val) => { ... }
|
|
8
|
+
* timeout(5000) => { ... }
|
|
9
|
+
* _ => { ... }
|
|
10
|
+
* }
|
|
11
|
+
*/
|
|
12
|
+
export class SelectStatement {
|
|
13
|
+
constructor(cases, loc) {
|
|
14
|
+
this.type = 'SelectStatement';
|
|
15
|
+
this.cases = cases; // Array of SelectCase
|
|
16
|
+
this.loc = loc;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A single case arm inside a select block.
|
|
22
|
+
*
|
|
23
|
+
* kind: "receive" | "send" | "timeout" | "default"
|
|
24
|
+
* channel: Expression | null (identifier for the channel; null for timeout/default)
|
|
25
|
+
* binding: string | null (variable name bound on receive; null otherwise)
|
|
26
|
+
* value: Expression | null (value to send, or timeout duration; null for receive/default)
|
|
27
|
+
* body: [Statement] (statements executed when this case fires)
|
|
28
|
+
*/
|
|
29
|
+
export class SelectCase {
|
|
30
|
+
constructor(kind, channel, binding, value, body, loc) {
|
|
31
|
+
this.type = 'SelectCase';
|
|
32
|
+
this.kind = kind; // "receive" | "send" | "timeout" | "default"
|
|
33
|
+
this.channel = channel; // Expression | null
|
|
34
|
+
this.binding = binding; // string | null
|
|
35
|
+
this.value = value; // Expression | null
|
|
36
|
+
this.body = body; // [Statement]
|
|
37
|
+
this.loc = loc;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Theme-specific AST Node definitions for the Tova language
|
|
2
|
+
// Extracted for lazy loading — only loaded when theme { } blocks are used.
|
|
3
|
+
|
|
4
|
+
export class ThemeBlock {
|
|
5
|
+
constructor(sections, darkOverrides, loc) {
|
|
6
|
+
this.type = 'ThemeBlock';
|
|
7
|
+
this.sections = sections; // Array of ThemeSection
|
|
8
|
+
this.darkOverrides = darkOverrides; // Array of ThemeToken (flat dark mode overrides)
|
|
9
|
+
this.loc = loc;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class ThemeSection {
|
|
14
|
+
constructor(name, tokens, loc) {
|
|
15
|
+
this.type = 'ThemeSection';
|
|
16
|
+
this.name = name; // string — section name, e.g. "colors", "spacing", "font"
|
|
17
|
+
this.tokens = tokens; // Array of ThemeToken
|
|
18
|
+
this.loc = loc;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class ThemeToken {
|
|
23
|
+
constructor(name, value, loc) {
|
|
24
|
+
this.type = 'ThemeToken';
|
|
25
|
+
this.name = name; // string — dot-separated name, e.g. "primary.hover"
|
|
26
|
+
this.value = value; // string or number — token value
|
|
27
|
+
this.loc = loc;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Theme-specific parser methods for the Tova language
|
|
2
|
+
// Extracted from parser.js for lazy loading — only loaded when theme { } blocks are encountered.
|
|
3
|
+
|
|
4
|
+
import { TokenType } from '../lexer/tokens.js';
|
|
5
|
+
import { ThemeBlock, ThemeSection, ThemeToken } from './theme-ast.js';
|
|
6
|
+
|
|
7
|
+
export function installThemeParser(ParserClass) {
|
|
8
|
+
if (ParserClass.prototype._themeParserInstalled) return;
|
|
9
|
+
ParserClass.prototype._themeParserInstalled = true;
|
|
10
|
+
|
|
11
|
+
ParserClass.prototype.parseThemeBlock = function() {
|
|
12
|
+
const l = this.loc();
|
|
13
|
+
this.advance(); // consume 'theme'
|
|
14
|
+
this.expect(TokenType.LBRACE, "Expected '{' after 'theme'");
|
|
15
|
+
|
|
16
|
+
const sections = [];
|
|
17
|
+
const darkOverrides = [];
|
|
18
|
+
|
|
19
|
+
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
20
|
+
const sectionLoc = this.loc();
|
|
21
|
+
const sectionName = this.expect(TokenType.IDENTIFIER, "Expected section name inside theme block").value;
|
|
22
|
+
this.expect(TokenType.LBRACE, `Expected '{' after theme section '${sectionName}'`);
|
|
23
|
+
|
|
24
|
+
if (sectionName === 'dark') {
|
|
25
|
+
// dark section: flat overrides with dot-notation names
|
|
26
|
+
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
27
|
+
darkOverrides.push(this._parseThemeToken());
|
|
28
|
+
}
|
|
29
|
+
} else {
|
|
30
|
+
// Regular section: parse tokens into a ThemeSection
|
|
31
|
+
const tokens = [];
|
|
32
|
+
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
33
|
+
tokens.push(this._parseThemeToken());
|
|
34
|
+
}
|
|
35
|
+
sections.push(new ThemeSection(sectionName, tokens, sectionLoc));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
this.expect(TokenType.RBRACE, `Expected '}' to close theme section '${sectionName}'`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
this.expect(TokenType.RBRACE, "Expected '}' to close theme block");
|
|
42
|
+
return new ThemeBlock(sections, darkOverrides, l);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
ParserClass.prototype._parseThemeToken = function() {
|
|
46
|
+
const l = this.loc();
|
|
47
|
+
|
|
48
|
+
// Read dot-separated name: IDENTIFIER (DOT IDENTIFIER)*
|
|
49
|
+
let name = this.expect(TokenType.IDENTIFIER, "Expected token name").value;
|
|
50
|
+
while (this.check(TokenType.DOT)) {
|
|
51
|
+
this.advance(); // consume DOT
|
|
52
|
+
const part = this.expect(TokenType.IDENTIFIER, "Expected identifier after '.' in token name").value;
|
|
53
|
+
name += '.' + part;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
this.expect(TokenType.COLON, `Expected ':' after token name '${name}'`);
|
|
57
|
+
|
|
58
|
+
// Read value: STRING or NUMBER
|
|
59
|
+
let value;
|
|
60
|
+
if (this.check(TokenType.STRING)) {
|
|
61
|
+
value = this.advance().value;
|
|
62
|
+
} else if (this.check(TokenType.NUMBER)) {
|
|
63
|
+
value = this.advance().value;
|
|
64
|
+
} else {
|
|
65
|
+
this.error(`Expected string or number value for token '${name}'`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return new ThemeToken(name, value, l);
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { TokenType } from '../../lexer/tokens.js';
|
|
2
|
+
import { installConcurrencyParser } from '../../parser/concurrency-parser.js';
|
|
3
|
+
|
|
4
|
+
export const concurrencyPlugin = {
|
|
5
|
+
name: 'concurrency',
|
|
6
|
+
astNodeType: 'ConcurrentBlock',
|
|
7
|
+
detection: {
|
|
8
|
+
strategy: 'identifier',
|
|
9
|
+
identifierValue: 'concurrent',
|
|
10
|
+
lookahead: (parser) => {
|
|
11
|
+
const next = parser.peek(1);
|
|
12
|
+
// concurrent {} or concurrent mode {}
|
|
13
|
+
return next.type === TokenType.LBRACE ||
|
|
14
|
+
(next.type === TokenType.IDENTIFIER &&
|
|
15
|
+
['cancel_on_error', 'first', 'timeout'].includes(next.value));
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
parser: {
|
|
19
|
+
install: installConcurrencyParser,
|
|
20
|
+
installedFlag: '_concurrencyParserInstalled',
|
|
21
|
+
method: 'parseConcurrentBlock',
|
|
22
|
+
},
|
|
23
|
+
analyzer: {
|
|
24
|
+
visit: (analyzer, node) => {
|
|
25
|
+
if (node.type === 'SelectStatement') return analyzer.visitSelectStatement(node);
|
|
26
|
+
return analyzer.visitConcurrentBlock(node);
|
|
27
|
+
},
|
|
28
|
+
childNodeTypes: ['SelectStatement'],
|
|
29
|
+
noopNodeTypes: ['SpawnExpression', 'SelectCase'],
|
|
30
|
+
},
|
|
31
|
+
codegen: {},
|
|
32
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { installDeployParser } from '../../parser/deploy-parser.js';
|
|
2
|
+
import { installDeployAnalyzer } from '../../analyzer/deploy-analyzer.js';
|
|
3
|
+
import { TokenType } from '../../lexer/tokens.js';
|
|
4
|
+
|
|
5
|
+
export const deployPlugin = {
|
|
6
|
+
name: 'deploy',
|
|
7
|
+
astNodeType: 'DeployBlock',
|
|
8
|
+
detection: {
|
|
9
|
+
strategy: 'identifier',
|
|
10
|
+
identifierValue: 'deploy',
|
|
11
|
+
lookahead: (parser) => {
|
|
12
|
+
const next = parser.peek(1);
|
|
13
|
+
// deploy "name" {} — name is required
|
|
14
|
+
return next.type === TokenType.STRING;
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
parser: {
|
|
18
|
+
install: installDeployParser,
|
|
19
|
+
installedFlag: '_deployParserInstalled',
|
|
20
|
+
method: 'parseDeployBlock',
|
|
21
|
+
},
|
|
22
|
+
analyzer: {
|
|
23
|
+
visit: (analyzer, node) => {
|
|
24
|
+
installDeployAnalyzer(analyzer.constructor);
|
|
25
|
+
analyzer.visitDeployBlock(node);
|
|
26
|
+
},
|
|
27
|
+
childNodeTypes: [],
|
|
28
|
+
noopNodeTypes: [
|
|
29
|
+
'DeployConfigField', 'DeployEnvBlock', 'DeployDbBlock',
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
codegen: {},
|
|
33
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { installThemeParser } from '../../parser/theme-parser.js';
|
|
2
|
+
|
|
3
|
+
export const themePlugin = {
|
|
4
|
+
name: 'theme',
|
|
5
|
+
astNodeType: 'ThemeBlock',
|
|
6
|
+
detection: {
|
|
7
|
+
strategy: 'identifier',
|
|
8
|
+
identifierValue: 'theme',
|
|
9
|
+
},
|
|
10
|
+
parser: {
|
|
11
|
+
install: installThemeParser,
|
|
12
|
+
installedFlag: '_themeParserInstalled',
|
|
13
|
+
method: 'parseThemeBlock',
|
|
14
|
+
},
|
|
15
|
+
analyzer: {
|
|
16
|
+
visit: (analyzer, node) => analyzer.visitThemeBlock(node),
|
|
17
|
+
noopNodeTypes: ['ThemeSection', 'ThemeToken'],
|
|
18
|
+
},
|
|
19
|
+
codegen: {},
|
|
20
|
+
};
|
|
@@ -11,7 +11,11 @@ import { dataPlugin } from './plugins/data-plugin.js';
|
|
|
11
11
|
import { testPlugin } from './plugins/test-plugin.js';
|
|
12
12
|
import { benchPlugin } from './plugins/bench-plugin.js';
|
|
13
13
|
import { edgePlugin } from './plugins/edge-plugin.js';
|
|
14
|
+
import { concurrencyPlugin } from './plugins/concurrency-plugin.js';
|
|
15
|
+
import { deployPlugin } from './plugins/deploy-plugin.js';
|
|
16
|
+
import { themePlugin } from './plugins/theme-plugin.js';
|
|
14
17
|
|
|
18
|
+
BlockRegistry.register(themePlugin);
|
|
15
19
|
BlockRegistry.register(serverPlugin);
|
|
16
20
|
BlockRegistry.register(browserPlugin);
|
|
17
21
|
BlockRegistry.register(sharedPlugin);
|
|
@@ -21,5 +25,7 @@ BlockRegistry.register(dataPlugin);
|
|
|
21
25
|
BlockRegistry.register(testPlugin);
|
|
22
26
|
BlockRegistry.register(benchPlugin);
|
|
23
27
|
BlockRegistry.register(edgePlugin);
|
|
28
|
+
BlockRegistry.register(concurrencyPlugin);
|
|
29
|
+
BlockRegistry.register(deployPlugin);
|
|
24
30
|
|
|
25
31
|
export { BlockRegistry };
|