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.
Files changed (59) hide show
  1. package/bin/tova.js +1312 -139
  2. package/package.json +8 -1
  3. package/src/analyzer/analyzer.js +539 -11
  4. package/src/analyzer/browser-analyzer.js +56 -8
  5. package/src/analyzer/deploy-analyzer.js +44 -0
  6. package/src/analyzer/scope.js +7 -0
  7. package/src/analyzer/server-analyzer.js +33 -1
  8. package/src/codegen/base-codegen.js +1296 -23
  9. package/src/codegen/browser-codegen.js +725 -20
  10. package/src/codegen/codegen.js +87 -5
  11. package/src/codegen/deploy-codegen.js +49 -0
  12. package/src/codegen/server-codegen.js +54 -6
  13. package/src/codegen/shared-codegen.js +5 -0
  14. package/src/codegen/theme-codegen.js +69 -0
  15. package/src/codegen/wasm-codegen.js +6 -0
  16. package/src/config/edit-toml.js +6 -2
  17. package/src/config/git-resolver.js +128 -0
  18. package/src/config/lock-file.js +57 -0
  19. package/src/config/module-cache.js +58 -0
  20. package/src/config/module-entry.js +37 -0
  21. package/src/config/module-path.js +63 -0
  22. package/src/config/pkg-errors.js +62 -0
  23. package/src/config/resolve.js +26 -0
  24. package/src/config/resolver.js +139 -0
  25. package/src/config/search.js +28 -0
  26. package/src/config/semver.js +72 -0
  27. package/src/config/toml.js +61 -6
  28. package/src/deploy/deploy.js +217 -0
  29. package/src/deploy/infer.js +218 -0
  30. package/src/deploy/provision.js +315 -0
  31. package/src/diagnostics/security-scorecard.js +111 -0
  32. package/src/lexer/lexer.js +18 -3
  33. package/src/lsp/server.js +482 -0
  34. package/src/parser/animate-ast.js +45 -0
  35. package/src/parser/ast.js +39 -0
  36. package/src/parser/browser-ast.js +19 -1
  37. package/src/parser/browser-parser.js +221 -4
  38. package/src/parser/concurrency-ast.js +15 -0
  39. package/src/parser/concurrency-parser.js +236 -0
  40. package/src/parser/deploy-ast.js +37 -0
  41. package/src/parser/deploy-parser.js +132 -0
  42. package/src/parser/parser.js +42 -5
  43. package/src/parser/select-ast.js +39 -0
  44. package/src/parser/theme-ast.js +29 -0
  45. package/src/parser/theme-parser.js +70 -0
  46. package/src/registry/plugins/concurrency-plugin.js +32 -0
  47. package/src/registry/plugins/deploy-plugin.js +33 -0
  48. package/src/registry/plugins/theme-plugin.js +20 -0
  49. package/src/registry/register-all.js +6 -0
  50. package/src/runtime/charts.js +547 -0
  51. package/src/runtime/embedded.js +6 -2
  52. package/src/runtime/reactivity.js +60 -0
  53. package/src/runtime/router.js +703 -295
  54. package/src/runtime/table.js +606 -33
  55. package/src/stdlib/inline.js +365 -10
  56. package/src/stdlib/runtime-bridge.js +152 -0
  57. package/src/stdlib/string.js +84 -2
  58. package/src/stdlib/validation.js +1 -1
  59. package/src/version.js +1 -1
@@ -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.IDENTIFIER) && this.current().value === '_') {
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 => ({ key: typeof p.key === 'string' ? p.key : p.key.name || p.key, value: typeof p.key === 'string' ? p.key : p.key.name || p.key })),
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 => e.type === 'Identifier' ? e.name : '_'),
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 };