tova 0.5.1 → 0.8.2

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 (60) hide show
  1. package/bin/tova.js +261 -60
  2. package/package.json +1 -1
  3. package/src/analyzer/analyzer.js +351 -11
  4. package/src/analyzer/{client-analyzer.js → browser-analyzer.js} +20 -17
  5. package/src/analyzer/deploy-analyzer.js +44 -0
  6. package/src/analyzer/form-analyzer.js +113 -0
  7. package/src/analyzer/scope.js +2 -2
  8. package/src/codegen/base-codegen.js +1160 -10
  9. package/src/codegen/{client-codegen.js → browser-codegen.js} +444 -5
  10. package/src/codegen/codegen.js +119 -28
  11. package/src/codegen/deploy-codegen.js +49 -0
  12. package/src/codegen/edge-codegen.js +1351 -0
  13. package/src/codegen/form-codegen.js +553 -0
  14. package/src/codegen/security-codegen.js +5 -5
  15. package/src/codegen/server-codegen.js +88 -7
  16. package/src/codegen/shared-codegen.js +5 -0
  17. package/src/codegen/wasm-codegen.js +6 -0
  18. package/src/config/edit-toml.js +6 -2
  19. package/src/config/git-resolver.js +128 -0
  20. package/src/config/lock-file.js +57 -0
  21. package/src/config/module-cache.js +58 -0
  22. package/src/config/module-entry.js +37 -0
  23. package/src/config/module-path.js +31 -0
  24. package/src/config/pkg-errors.js +62 -0
  25. package/src/config/resolve.js +17 -0
  26. package/src/config/resolver.js +139 -0
  27. package/src/config/search.js +28 -0
  28. package/src/config/semver.js +72 -0
  29. package/src/config/toml.js +48 -5
  30. package/src/deploy/deploy.js +217 -0
  31. package/src/deploy/infer.js +218 -0
  32. package/src/deploy/provision.js +311 -0
  33. package/src/diagnostics/error-codes.js +1 -1
  34. package/src/docs/generator.js +1 -1
  35. package/src/formatter/formatter.js +4 -4
  36. package/src/lexer/tokens.js +12 -2
  37. package/src/lsp/server.js +483 -1
  38. package/src/parser/ast.js +60 -5
  39. package/src/parser/{client-ast.js → browser-ast.js} +3 -3
  40. package/src/parser/{client-parser.js → browser-parser.js} +42 -15
  41. package/src/parser/concurrency-ast.js +15 -0
  42. package/src/parser/concurrency-parser.js +236 -0
  43. package/src/parser/deploy-ast.js +37 -0
  44. package/src/parser/deploy-parser.js +132 -0
  45. package/src/parser/edge-ast.js +83 -0
  46. package/src/parser/edge-parser.js +262 -0
  47. package/src/parser/form-ast.js +80 -0
  48. package/src/parser/form-parser.js +206 -0
  49. package/src/parser/parser.js +82 -14
  50. package/src/parser/select-ast.js +39 -0
  51. package/src/registry/plugins/browser-plugin.js +30 -0
  52. package/src/registry/plugins/concurrency-plugin.js +32 -0
  53. package/src/registry/plugins/deploy-plugin.js +33 -0
  54. package/src/registry/plugins/edge-plugin.js +32 -0
  55. package/src/registry/register-all.js +8 -2
  56. package/src/runtime/ssr.js +2 -2
  57. package/src/stdlib/inline.js +38 -6
  58. package/src/stdlib/runtime-bridge.js +152 -0
  59. package/src/version.js +1 -1
  60. package/src/registry/plugins/client-plugin.js +0 -30
@@ -1,42 +1,55 @@
1
- // Client-specific parser methods for the Tova language
2
- // Extracted from parser.js for lazy loading — only loaded when client { } blocks are encountered.
1
+ // Browser-specific parser methods for the Tova language
2
+ // Extracted from parser.js for lazy loading — only loaded when browser { } blocks are encountered.
3
3
 
4
4
  import { TokenType, Keywords } from '../lexer/tokens.js';
5
5
  import * as AST from './ast.js';
6
+ import { installFormParser } from './form-parser.js';
6
7
 
7
- export function installClientParser(ParserClass) {
8
- if (ParserClass.prototype._clientParserInstalled) return;
9
- ParserClass.prototype._clientParserInstalled = true;
8
+ export function installBrowserParser(ParserClass) {
9
+ if (ParserClass.prototype._browserParserInstalled) return;
10
+ ParserClass.prototype._browserParserInstalled = true;
10
11
 
11
- ParserClass.prototype.parseClientBlock = function() {
12
+ installFormParser(ParserClass);
13
+
14
+ ParserClass.prototype.parseBrowserBlock = function() {
12
15
  const l = this.loc();
13
- this.expect(TokenType.CLIENT);
14
- // Optional block name: client "admin" { }
16
+ // Capture the keyword value before consuming for deprecation warning
17
+ const keyword = this.current().value;
18
+ this.expect(TokenType.BROWSER);
19
+ if (keyword === 'client') {
20
+ this.warnings = this.warnings || [];
21
+ this.warnings.push({
22
+ message: "`client` block is deprecated, use `browser` instead",
23
+ loc: l,
24
+ });
25
+ }
26
+ // Optional block name: browser "admin" { }
15
27
  let name = null;
16
28
  if (this.check(TokenType.STRING)) {
17
29
  name = this.advance().value;
18
30
  }
19
- this.expect(TokenType.LBRACE, "Expected '{' after 'client'");
31
+ this.expect(TokenType.LBRACE, "Expected '{' after 'browser'");
20
32
  const body = [];
21
33
  while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
22
34
  try {
23
- const stmt = this.parseClientStatement();
35
+ const stmt = this.parseBrowserStatement();
24
36
  if (stmt) body.push(stmt);
25
37
  } catch (e) {
26
38
  this.errors.push(e);
27
39
  this._synchronizeBlock();
28
40
  }
29
41
  }
30
- this.expect(TokenType.RBRACE, "Expected '}' to close client block");
31
- return new AST.ClientBlock(body, l, name);
42
+ this.expect(TokenType.RBRACE, "Expected '}' to close browser block");
43
+ return new AST.BrowserBlock(body, l, name);
32
44
  };
33
45
 
34
- ParserClass.prototype.parseClientStatement = function() {
46
+ ParserClass.prototype.parseBrowserStatement = function() {
35
47
  if (this.check(TokenType.STATE)) return this.parseState();
36
48
  if (this.check(TokenType.COMPUTED)) return this.parseComputed();
37
49
  if (this.check(TokenType.EFFECT)) return this.parseEffect();
38
50
  if (this.check(TokenType.COMPONENT)) return this.parseComponent();
39
51
  if (this.check(TokenType.STORE)) return this.parseStore();
52
+ if (this.check(TokenType.FORM)) return this.parseFormDeclaration();
40
53
  return this.parseStatement();
41
54
  };
42
55
 
@@ -125,6 +138,8 @@ export function installClientParser(ParserClass) {
125
138
  body.push(this.parseEffect());
126
139
  } else if (this.check(TokenType.COMPONENT)) {
127
140
  body.push(this.parseComponent());
141
+ } else if (this.check(TokenType.FORM)) {
142
+ body.push(this.parseFormDeclaration());
128
143
  } else {
129
144
  body.push(this.parseStatement());
130
145
  }
@@ -242,7 +257,13 @@ export function installClientParser(ParserClass) {
242
257
  const l = this.loc();
243
258
  this.expect(TokenType.LESS, "Expected '<'");
244
259
 
245
- const tag = this.expect(TokenType.IDENTIFIER, "Expected tag name").value;
260
+ // Accept identifiers and keywords as JSX tag names (e.g., <form>, <label>)
261
+ let tag;
262
+ if (this.check(TokenType.IDENTIFIER) || (this.peek().value in Keywords)) {
263
+ tag = this.advance().value;
264
+ } else {
265
+ tag = this.expect(TokenType.IDENTIFIER, "Expected tag name").value;
266
+ }
246
267
 
247
268
  // Parse attributes (including spread: {...expr})
248
269
  const attributes = [];
@@ -330,7 +351,13 @@ export function installClientParser(ParserClass) {
330
351
  if (this.check(TokenType.LESS) && this.peek(1).type === TokenType.SLASH) {
331
352
  this.advance(); // <
332
353
  this.advance(); // /
333
- const closeTag = this.expect(TokenType.IDENTIFIER, "Expected closing tag name").value;
354
+ // Accept identifiers and keywords as JSX closing tag names (e.g., </form>)
355
+ let closeTag;
356
+ if (this.check(TokenType.IDENTIFIER) || (this.peek().value in Keywords)) {
357
+ closeTag = this.advance().value;
358
+ } else {
359
+ closeTag = this.expect(TokenType.IDENTIFIER, "Expected closing tag name").value;
360
+ }
334
361
  if (closeTag !== parentTag) {
335
362
  this.error(`Mismatched closing tag: expected </${parentTag}>, got </${closeTag}>`);
336
363
  }
@@ -0,0 +1,15 @@
1
+ // Concurrency-specific AST Node definitions for the Tova language
2
+ // Extracted for lazy loading -- only loaded when concurrent { } blocks are used.
3
+
4
+ /**
5
+ * spawn foo(args)
6
+ * spawn fn() { ... }
7
+ */
8
+ export class SpawnExpression {
9
+ constructor(callee, args, loc) {
10
+ this.type = 'SpawnExpression';
11
+ this.callee = callee; // Expression (function name or lambda)
12
+ this.arguments = args; // Array of Expression
13
+ this.loc = loc;
14
+ }
15
+ }
@@ -0,0 +1,236 @@
1
+ // Concurrency-specific parser methods for the Tova language
2
+ // Extracted for lazy loading — only loaded when concurrent { } blocks are used.
3
+
4
+ import { TokenType } from '../lexer/tokens.js';
5
+ import * as AST from './ast.js';
6
+ import { SpawnExpression } from './concurrency-ast.js';
7
+ import { SelectStatement, SelectCase } from './select-ast.js';
8
+
9
+ const CONCURRENT_MODES = new Set(['cancel_on_error', 'first', 'timeout']);
10
+
11
+ export function installConcurrencyParser(ParserClass) {
12
+ if (ParserClass.prototype._concurrencyParserInstalled) return;
13
+ ParserClass.prototype._concurrencyParserInstalled = true;
14
+
15
+ /**
16
+ * Parse: concurrent [mode] { body }
17
+ *
18
+ * Modes:
19
+ * concurrent { ... } — mode "all" (default)
20
+ * concurrent cancel_on_error { ... } — cancel siblings on first error
21
+ * concurrent first { ... } — return first result, cancel rest
22
+ * concurrent timeout(ms) { ... } — timeout after ms milliseconds
23
+ */
24
+ ParserClass.prototype.parseConcurrentBlock = function() {
25
+ const l = this.loc();
26
+ this.advance(); // consume 'concurrent'
27
+
28
+ let mode = 'all';
29
+ let timeout = null;
30
+
31
+ // Check for mode modifier
32
+ if (this.check(TokenType.IDENTIFIER) && CONCURRENT_MODES.has(this.current().value)) {
33
+ const modeName = this.advance().value;
34
+ if (modeName === 'timeout') {
35
+ this.expect(TokenType.LPAREN, "Expected '(' after 'timeout'");
36
+ timeout = this.parseExpression();
37
+ this.expect(TokenType.RPAREN, "Expected ')' after timeout value");
38
+ mode = 'timeout';
39
+ } else {
40
+ mode = modeName;
41
+ }
42
+ }
43
+
44
+ this.expect(TokenType.LBRACE, "Expected '{' after 'concurrent'");
45
+
46
+ const body = [];
47
+ while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
48
+ try {
49
+ const stmt = this.parseStatement();
50
+ if (stmt) body.push(stmt);
51
+ } catch (e) {
52
+ this.errors.push(e);
53
+ this._synchronizeBlock();
54
+ }
55
+ }
56
+
57
+ this.expect(TokenType.RBRACE, "Expected '}' to close concurrent block");
58
+ return new AST.ConcurrentBlock(mode, timeout, body, l);
59
+ };
60
+
61
+ // Save the original parseUnary method to extend it with spawn support
62
+ const _originalParseUnary = ParserClass.prototype.parseUnary;
63
+
64
+ /**
65
+ * Extend parseUnary to handle `spawn` as a prefix expression.
66
+ * spawn foo(args) → SpawnExpression
67
+ * Works like `await` but for concurrent task spawning.
68
+ */
69
+ ParserClass.prototype.parseUnary = function() {
70
+ if (this.check(TokenType.IDENTIFIER) && this.current().value === 'spawn') {
71
+ // Distinguish concurrency `spawn foo()` from stdlib function call `spawn("cmd", args)`.
72
+ // If `spawn` is followed by `(`, it's a regular function call, not a concurrency keyword.
73
+ const next = this.peek(1);
74
+ if (next && next.type === TokenType.LPAREN) {
75
+ return _originalParseUnary.call(this);
76
+ }
77
+
78
+ const l = this.loc();
79
+ this.advance(); // consume 'spawn'
80
+
81
+ // Parse the expression after spawn (function call, lambda, etc.)
82
+ const expr = this.parseUnary();
83
+
84
+ // If it's a call expression, split into callee + args
85
+ if (expr.type === 'CallExpression') {
86
+ return new SpawnExpression(expr.callee, expr.arguments, l);
87
+ }
88
+
89
+ // Otherwise treat the whole expression as the callee with no args
90
+ return new SpawnExpression(expr, [], l);
91
+ }
92
+
93
+ return _originalParseUnary.call(this);
94
+ };
95
+
96
+ // Also support concurrent as a statement inside function bodies
97
+ const _originalParseStatement = ParserClass.prototype.parseStatement;
98
+
99
+ ParserClass.prototype.parseStatement = function() {
100
+ // Check for 'concurrent' at statement level (inside function bodies)
101
+ if (this.check(TokenType.IDENTIFIER) && this.current().value === 'concurrent') {
102
+ return this.parseConcurrentBlock();
103
+ }
104
+ // Check for 'select {' at statement level (disambiguate from select() function call)
105
+ if (this.check(TokenType.IDENTIFIER) && this.current().value === 'select'
106
+ && this.peek(1).type === TokenType.LBRACE) {
107
+ return this.parseSelectStatement();
108
+ }
109
+ return _originalParseStatement.call(this);
110
+ };
111
+
112
+ /**
113
+ * Parse: select { case1 case2 ... }
114
+ *
115
+ * Each case is one of:
116
+ * binding from channel => body
117
+ * _ from channel => body
118
+ * channel.send(value) => body
119
+ * timeout(ms) => body
120
+ * _ => body
121
+ */
122
+ ParserClass.prototype.parseSelectStatement = function() {
123
+ const l = this.loc();
124
+ this.advance(); // consume 'select'
125
+ this.expect(TokenType.LBRACE, "Expected '{' after 'select'");
126
+
127
+ const cases = [];
128
+ while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
129
+ try {
130
+ cases.push(this.parseSelectCase());
131
+ } catch (e) {
132
+ this.errors.push(e);
133
+ this._synchronizeBlock();
134
+ }
135
+ }
136
+
137
+ this.expect(TokenType.RBRACE, "Expected '}' to close select block");
138
+ return new SelectStatement(cases, l);
139
+ };
140
+
141
+ ParserClass.prototype.parseSelectCase = function() {
142
+ const l = this.loc();
143
+
144
+ // timeout(ms) => body
145
+ if (this.check(TokenType.IDENTIFIER) && this.current().value === 'timeout'
146
+ && this.peek(1).type === TokenType.LPAREN) {
147
+ this.advance(); // consume 'timeout'
148
+ this.expect(TokenType.LPAREN, "Expected '(' after 'timeout'");
149
+ const ms = this.parseExpression();
150
+ this.expect(TokenType.RPAREN, "Expected ')' after timeout value");
151
+ this.expect(TokenType.ARROW, "Expected '=>' after timeout");
152
+ const body = this.parseSelectCaseBody();
153
+ return new SelectCase('timeout', null, null, ms, body, l);
154
+ }
155
+
156
+ // _ => body (default case) — must check before _ from channel
157
+ if (this.check(TokenType.IDENTIFIER) && this.current().value === '_'
158
+ && this.peek(1).type === TokenType.ARROW) {
159
+ this.advance(); // consume '_'
160
+ this.expect(TokenType.ARROW, "Expected '=>' after '_'");
161
+ const body = this.parseSelectCaseBody();
162
+ return new SelectCase('default', null, null, null, body, l);
163
+ }
164
+
165
+ // _ from channel => body (wildcard receive)
166
+ if (this.check(TokenType.IDENTIFIER) && this.current().value === '_'
167
+ && this.peek(1).type === TokenType.FROM) {
168
+ this.advance(); // consume '_'
169
+ this.advance(); // consume 'from'
170
+ const channel = this._parseSelectChannel();
171
+ this.expect(TokenType.ARROW, "Expected '=>' after channel");
172
+ const body = this.parseSelectCaseBody();
173
+ return new SelectCase('receive', channel, null, null, body, l);
174
+ }
175
+
176
+ // binding from channel => body (named receive)
177
+ if (this.check(TokenType.IDENTIFIER) && this.peek(1).type === TokenType.FROM) {
178
+ const binding = this.advance().value; // consume binding name
179
+ this.advance(); // consume 'from'
180
+ const channel = this._parseSelectChannel();
181
+ this.expect(TokenType.ARROW, "Expected '=>' after channel");
182
+ const body = this.parseSelectCaseBody();
183
+ return new SelectCase('receive', channel, binding, null, body, l);
184
+ }
185
+
186
+ // channel.send(value) => body (send case)
187
+ // Parse as expression, then check if it's a send call
188
+ const expr = this.parseExpression();
189
+ if (expr.type === 'CallExpression' && expr.callee.type === 'MemberExpression'
190
+ && expr.callee.property === 'send') {
191
+ this.expect(TokenType.ARROW, "Expected '=>' after send");
192
+ const body = this.parseSelectCaseBody();
193
+ return new SelectCase('send', expr.callee.object, null, expr.arguments[0], body, l);
194
+ }
195
+
196
+ throw this.error("Expected select case: 'binding from channel =>', 'timeout(ms) =>', 'channel.send(val) =>', or '_ =>'", l);
197
+ };
198
+
199
+ /**
200
+ * Parse a channel expression in a select receive case.
201
+ * Handles identifiers and member access chains (e.g., ch, obj.ch)
202
+ * without consuming '=>' as a lambda arrow.
203
+ */
204
+ ParserClass.prototype._parseSelectChannel = function() {
205
+ const l = this.loc();
206
+ let expr = new AST.Identifier(this.advance().value, l);
207
+ // Follow member access chains: ch.sub, obj.channels, etc.
208
+ while (this.check(TokenType.DOT)) {
209
+ this.advance(); // consume '.'
210
+ const prop = this.advance().value;
211
+ expr = new AST.MemberExpression(expr, prop, false, this.loc());
212
+ }
213
+ return expr;
214
+ };
215
+
216
+ ParserClass.prototype.parseSelectCaseBody = function() {
217
+ if (this.check(TokenType.LBRACE)) {
218
+ this.advance(); // consume '{'
219
+ const body = [];
220
+ while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
221
+ try {
222
+ const stmt = this.parseStatement();
223
+ if (stmt) body.push(stmt);
224
+ } catch (e) {
225
+ this.errors.push(e);
226
+ this._synchronizeBlock();
227
+ }
228
+ }
229
+ this.expect(TokenType.RBRACE, "Expected '}' to close select case body");
230
+ return body;
231
+ }
232
+ // Single statement
233
+ const stmt = this.parseStatement();
234
+ return stmt ? [stmt] : [];
235
+ };
236
+ }
@@ -0,0 +1,37 @@
1
+ // Deploy-specific AST Node definitions for the Tova language
2
+ // Extracted for lazy loading — only loaded when deploy { } blocks are used.
3
+
4
+ export class DeployBlock {
5
+ constructor(body, loc, name = null) {
6
+ this.type = 'DeployBlock';
7
+ this.name = name;
8
+ this.body = body;
9
+ this.loc = loc;
10
+ }
11
+ }
12
+
13
+ export class DeployConfigField {
14
+ constructor(key, value, loc) {
15
+ this.type = 'DeployConfigField';
16
+ this.key = key;
17
+ this.value = value;
18
+ this.loc = loc;
19
+ }
20
+ }
21
+
22
+ export class DeployEnvBlock {
23
+ constructor(entries, loc) {
24
+ this.type = 'DeployEnvBlock';
25
+ this.entries = entries;
26
+ this.loc = loc;
27
+ }
28
+ }
29
+
30
+ export class DeployDbBlock {
31
+ constructor(engine, config, loc) {
32
+ this.type = 'DeployDbBlock';
33
+ this.engine = engine;
34
+ this.config = config;
35
+ this.loc = loc;
36
+ }
37
+ }
@@ -0,0 +1,132 @@
1
+ // Deploy-specific parser methods for the Tova language
2
+ // Extracted from parser.js for lazy loading — only loaded when deploy { } blocks are encountered.
3
+
4
+ import { TokenType } from '../lexer/tokens.js';
5
+ import {
6
+ DeployBlock, DeployConfigField, DeployEnvBlock, DeployDbBlock,
7
+ } from './deploy-ast.js';
8
+
9
+ // Keywords that start sub-blocks (not config fields)
10
+ const DEPLOY_SUB_BLOCK_KEYWORDS = new Set(['env', 'db']);
11
+
12
+ export function installDeployParser(ParserClass) {
13
+ if (ParserClass.prototype._deployParserInstalled) return;
14
+ ParserClass.prototype._deployParserInstalled = true;
15
+
16
+ ParserClass.prototype.parseDeployBlock = function() {
17
+ const l = this.loc();
18
+ this.advance(); // consume 'deploy'
19
+
20
+ // Deploy blocks REQUIRE a name
21
+ if (!this.check(TokenType.STRING)) {
22
+ throw this.error("Deploy block requires a name (e.g., deploy \"prod\" { })");
23
+ }
24
+ const name = this.advance().value;
25
+
26
+ this.expect(TokenType.LBRACE, "Expected '{' after deploy name");
27
+ const body = [];
28
+
29
+ while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
30
+ try {
31
+ const stmt = this.parseDeployStatement();
32
+ if (stmt) {
33
+ if (Array.isArray(stmt)) {
34
+ body.push(...stmt);
35
+ } else {
36
+ body.push(stmt);
37
+ }
38
+ }
39
+ } catch (e) {
40
+ this.errors.push(e);
41
+ this._synchronizeBlock();
42
+ }
43
+ }
44
+
45
+ this.expect(TokenType.RBRACE, "Expected '}' to close deploy block");
46
+ return new DeployBlock(body, l, name);
47
+ };
48
+
49
+ ParserClass.prototype.parseDeployStatement = function() {
50
+ if (this.check(TokenType.IDENTIFIER)) {
51
+ const val = this.current().value;
52
+
53
+ // env { KEY: "value" }
54
+ if (val === 'env') {
55
+ return this.parseDeployEnvBlock();
56
+ }
57
+
58
+ // db { postgres { } redis { } }
59
+ if (val === 'db') {
60
+ return this.parseDeployDbBlock();
61
+ }
62
+
63
+ // Config field: identifier: value (e.g., domain: "myapp.com")
64
+ if (this.peek(1).type === TokenType.COLON && !DEPLOY_SUB_BLOCK_KEYWORDS.has(val)) {
65
+ return this.parseDeployConfigField();
66
+ }
67
+ }
68
+
69
+ // Handle keyword tokens used as config keys (e.g., server: "root@example.com")
70
+ // In deploy blocks, 'server' is lexed as TokenType.SERVER, not IDENTIFIER
71
+ if (this.check(TokenType.SERVER) && this.peek(1).type === TokenType.COLON) {
72
+ return this.parseDeployConfigField();
73
+ }
74
+
75
+ // Deploy blocks only contain config fields, env, and db sub-blocks
76
+ throw this.error(`Unexpected token in deploy block: "${this.current().value || this.current().type}"`);
77
+ };
78
+
79
+ ParserClass.prototype.parseDeployConfigField = function() {
80
+ const l = this.loc();
81
+ const key = this.advance().value; // consume identifier (e.g., 'server')
82
+ this.expect(TokenType.COLON, "Expected ':' after config key");
83
+ const value = this.parseExpression();
84
+ return new DeployConfigField(key, value, l);
85
+ };
86
+
87
+ ParserClass.prototype.parseDeployEnvBlock = function() {
88
+ const l = this.loc();
89
+ this.advance(); // consume 'env'
90
+ this.expect(TokenType.LBRACE, "Expected '{' after 'env'");
91
+ const entries = [];
92
+
93
+ while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
94
+ const key = this.expect(TokenType.IDENTIFIER, "Expected env variable name").value;
95
+ this.expect(TokenType.COLON, "Expected ':' after env key");
96
+ const value = this.parseExpression();
97
+ entries.push({ key, value });
98
+ this.match(TokenType.COMMA);
99
+ }
100
+
101
+ this.expect(TokenType.RBRACE, "Expected '}' to close env block");
102
+ return new DeployEnvBlock(entries, l);
103
+ };
104
+
105
+ ParserClass.prototype.parseDeployDbBlock = function() {
106
+ const l = this.loc();
107
+ this.advance(); // consume 'db'
108
+ this.expect(TokenType.LBRACE, "Expected '{' after 'db'");
109
+ const blocks = [];
110
+
111
+ while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
112
+ const engineLoc = this.loc();
113
+ const engine = this.expect(TokenType.IDENTIFIER, "Expected database engine name (e.g., postgres, redis)").value;
114
+ this.expect(TokenType.LBRACE, `Expected '{' after '${engine}'`);
115
+ const config = {};
116
+
117
+ while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
118
+ const key = this.expect(TokenType.IDENTIFIER, "Expected config key").value;
119
+ this.expect(TokenType.COLON, "Expected ':' after config key");
120
+ const value = this.parseExpression();
121
+ config[key] = value;
122
+ this.match(TokenType.COMMA);
123
+ }
124
+
125
+ this.expect(TokenType.RBRACE, `Expected '}' to close ${engine} config`);
126
+ blocks.push(new DeployDbBlock(engine, config, engineLoc));
127
+ }
128
+
129
+ this.expect(TokenType.RBRACE, "Expected '}' to close db block");
130
+ return blocks;
131
+ };
132
+ }
@@ -0,0 +1,83 @@
1
+ // Edge/serverless-specific AST Node definitions for the Tova language
2
+ // Extracted for lazy loading — only loaded when edge { } blocks are used.
3
+
4
+ export class EdgeConfigField {
5
+ constructor(key, value, loc) {
6
+ this.type = 'EdgeConfigField';
7
+ this.key = key; // string — "target", "name", etc.
8
+ this.value = value; // Expression (StringLiteral, etc.)
9
+ this.loc = loc;
10
+ }
11
+ }
12
+
13
+ export class EdgeKVDeclaration {
14
+ constructor(name, config, loc) {
15
+ this.type = 'EdgeKVDeclaration';
16
+ this.name = name; // string — binding name (e.g., "CACHE")
17
+ this.config = config; // object or null — { ttl: Expression } etc.
18
+ this.loc = loc;
19
+ }
20
+ }
21
+
22
+ export class EdgeSQLDeclaration {
23
+ constructor(name, config, loc) {
24
+ this.type = 'EdgeSQLDeclaration';
25
+ this.name = name; // string — binding name (e.g., "DB")
26
+ this.config = config; // object or null
27
+ this.loc = loc;
28
+ }
29
+ }
30
+
31
+ export class EdgeStorageDeclaration {
32
+ constructor(name, config, loc) {
33
+ this.type = 'EdgeStorageDeclaration';
34
+ this.name = name; // string — binding name (e.g., "UPLOADS")
35
+ this.config = config; // object or null
36
+ this.loc = loc;
37
+ }
38
+ }
39
+
40
+ export class EdgeQueueDeclaration {
41
+ constructor(name, config, loc) {
42
+ this.type = 'EdgeQueueDeclaration';
43
+ this.name = name; // string — binding name (e.g., "EMAILS")
44
+ this.config = config; // object or null
45
+ this.loc = loc;
46
+ }
47
+ }
48
+
49
+ export class EdgeEnvDeclaration {
50
+ constructor(name, defaultValue, loc) {
51
+ this.type = 'EdgeEnvDeclaration';
52
+ this.name = name; // string — env var name
53
+ this.defaultValue = defaultValue; // Expression or null
54
+ this.loc = loc;
55
+ }
56
+ }
57
+
58
+ export class EdgeSecretDeclaration {
59
+ constructor(name, loc) {
60
+ this.type = 'EdgeSecretDeclaration';
61
+ this.name = name; // string — secret name
62
+ this.loc = loc;
63
+ }
64
+ }
65
+
66
+ export class EdgeScheduleDeclaration {
67
+ constructor(name, cron, body, loc) {
68
+ this.type = 'EdgeScheduleDeclaration';
69
+ this.name = name; // string — schedule name
70
+ this.cron = cron; // string — cron expression
71
+ this.body = body; // BlockStatement
72
+ this.loc = loc;
73
+ }
74
+ }
75
+
76
+ export class EdgeConsumeDeclaration {
77
+ constructor(queue, handler, loc) {
78
+ this.type = 'EdgeConsumeDeclaration';
79
+ this.queue = queue; // string — queue binding name
80
+ this.handler = handler; // FunctionDeclaration or LambdaExpression
81
+ this.loc = loc;
82
+ }
83
+ }