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.
- package/bin/tova.js +261 -60
- package/package.json +1 -1
- package/src/analyzer/analyzer.js +351 -11
- package/src/analyzer/{client-analyzer.js → browser-analyzer.js} +20 -17
- package/src/analyzer/deploy-analyzer.js +44 -0
- package/src/analyzer/form-analyzer.js +113 -0
- package/src/analyzer/scope.js +2 -2
- package/src/codegen/base-codegen.js +1160 -10
- package/src/codegen/{client-codegen.js → browser-codegen.js} +444 -5
- package/src/codegen/codegen.js +119 -28
- package/src/codegen/deploy-codegen.js +49 -0
- package/src/codegen/edge-codegen.js +1351 -0
- package/src/codegen/form-codegen.js +553 -0
- package/src/codegen/security-codegen.js +5 -5
- package/src/codegen/server-codegen.js +88 -7
- package/src/codegen/shared-codegen.js +5 -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 +31 -0
- package/src/config/pkg-errors.js +62 -0
- package/src/config/resolve.js +17 -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 +48 -5
- package/src/deploy/deploy.js +217 -0
- package/src/deploy/infer.js +218 -0
- package/src/deploy/provision.js +311 -0
- package/src/diagnostics/error-codes.js +1 -1
- package/src/docs/generator.js +1 -1
- package/src/formatter/formatter.js +4 -4
- package/src/lexer/tokens.js +12 -2
- package/src/lsp/server.js +483 -1
- package/src/parser/ast.js +60 -5
- package/src/parser/{client-ast.js → browser-ast.js} +3 -3
- package/src/parser/{client-parser.js → browser-parser.js} +42 -15
- 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/edge-ast.js +83 -0
- package/src/parser/edge-parser.js +262 -0
- package/src/parser/form-ast.js +80 -0
- package/src/parser/form-parser.js +206 -0
- package/src/parser/parser.js +82 -14
- package/src/parser/select-ast.js +39 -0
- package/src/registry/plugins/browser-plugin.js +30 -0
- package/src/registry/plugins/concurrency-plugin.js +32 -0
- package/src/registry/plugins/deploy-plugin.js +33 -0
- package/src/registry/plugins/edge-plugin.js +32 -0
- package/src/registry/register-all.js +8 -2
- package/src/runtime/ssr.js +2 -2
- package/src/stdlib/inline.js +38 -6
- package/src/stdlib/runtime-bridge.js +152 -0
- package/src/version.js +1 -1
- package/src/registry/plugins/client-plugin.js +0 -30
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
// Edge/serverless-specific parser methods for the Tova language
|
|
2
|
+
// Extracted from parser.js for lazy loading — only loaded when edge { } blocks are encountered.
|
|
3
|
+
|
|
4
|
+
import { TokenType } from '../lexer/tokens.js';
|
|
5
|
+
import * as AST from './ast.js';
|
|
6
|
+
import { installServerParser } from './server-parser.js';
|
|
7
|
+
import {
|
|
8
|
+
EdgeConfigField, EdgeKVDeclaration, EdgeSQLDeclaration,
|
|
9
|
+
EdgeStorageDeclaration, EdgeQueueDeclaration, EdgeEnvDeclaration,
|
|
10
|
+
EdgeSecretDeclaration, EdgeScheduleDeclaration, EdgeConsumeDeclaration,
|
|
11
|
+
} from './edge-ast.js';
|
|
12
|
+
|
|
13
|
+
// Valid config keys inside edge blocks
|
|
14
|
+
const EDGE_CONFIG_KEYS = new Set(['target']);
|
|
15
|
+
|
|
16
|
+
// Valid edge targets
|
|
17
|
+
const EDGE_TARGETS = new Set(['cloudflare', 'deno', 'vercel', 'lambda', 'bun']);
|
|
18
|
+
|
|
19
|
+
// Edge binding keywords (contextual identifiers)
|
|
20
|
+
const EDGE_BINDING_KEYWORDS = new Set(['kv', 'sql', 'storage', 'queue', 'env', 'secret']);
|
|
21
|
+
|
|
22
|
+
export function installEdgeParser(ParserClass) {
|
|
23
|
+
if (ParserClass.prototype._edgeParserInstalled) return;
|
|
24
|
+
ParserClass.prototype._edgeParserInstalled = true;
|
|
25
|
+
|
|
26
|
+
// Edge reuses parseRoute() and parseMiddleware() from the server parser
|
|
27
|
+
installServerParser(ParserClass);
|
|
28
|
+
|
|
29
|
+
ParserClass.prototype.parseEdgeBlock = function() {
|
|
30
|
+
const l = this.loc();
|
|
31
|
+
this.advance(); // consume 'edge'
|
|
32
|
+
|
|
33
|
+
// Optional name: edge "api" { }
|
|
34
|
+
let name = null;
|
|
35
|
+
if (this.check(TokenType.STRING)) {
|
|
36
|
+
name = this.advance().value;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
this.expect(TokenType.LBRACE, "Expected '{' after 'edge'");
|
|
40
|
+
const body = [];
|
|
41
|
+
|
|
42
|
+
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
43
|
+
try {
|
|
44
|
+
const stmt = this.parseEdgeStatement();
|
|
45
|
+
if (stmt) body.push(stmt);
|
|
46
|
+
} catch (e) {
|
|
47
|
+
this.errors.push(e);
|
|
48
|
+
this._synchronizeBlock();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
this.expect(TokenType.RBRACE, "Expected '}' to close edge block");
|
|
53
|
+
return new AST.EdgeBlock(body, l, name);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
ParserClass.prototype.parseEdgeStatement = function() {
|
|
57
|
+
// route keyword → reuse server route parser
|
|
58
|
+
if (this.check(TokenType.ROUTE)) {
|
|
59
|
+
// Ensure server parser is installed (for parseRoute)
|
|
60
|
+
return this.parseRoute();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Contextual keywords in edge blocks
|
|
64
|
+
if (this.check(TokenType.IDENTIFIER)) {
|
|
65
|
+
const val = this.current().value;
|
|
66
|
+
|
|
67
|
+
// middleware fn name(req, next) { ... }
|
|
68
|
+
if (val === 'middleware' && this.peek(1).type === TokenType.FN) {
|
|
69
|
+
return this.parseMiddleware();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// kv BINDING_NAME or kv BINDING_NAME { config }
|
|
73
|
+
if (val === 'kv') {
|
|
74
|
+
return this.parseEdgeKV();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// sql BINDING_NAME
|
|
78
|
+
if (val === 'sql') {
|
|
79
|
+
return this.parseEdgeSQL();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// storage BINDING_NAME
|
|
83
|
+
if (val === 'storage') {
|
|
84
|
+
return this.parseEdgeStorage();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// queue BINDING_NAME
|
|
88
|
+
if (val === 'queue') {
|
|
89
|
+
return this.parseEdgeQueue();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// env VAR_NAME = default_value
|
|
93
|
+
if (val === 'env') {
|
|
94
|
+
return this.parseEdgeEnv();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// secret SECRET_NAME
|
|
98
|
+
if (val === 'secret') {
|
|
99
|
+
return this.parseEdgeSecret();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// schedule "name" cron("...") { body }
|
|
103
|
+
if (val === 'schedule') {
|
|
104
|
+
return this.parseEdgeSchedule();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// consume QUEUE_NAME fn(messages) { body }
|
|
108
|
+
if (val === 'consume') {
|
|
109
|
+
return this.parseEdgeConsume();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// health "/path"
|
|
113
|
+
if (val === 'health') {
|
|
114
|
+
return this.parseHealthCheck();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// cors { ... }
|
|
118
|
+
if (val === 'cors') {
|
|
119
|
+
return this.parseCorsConfig();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// on_error fn(err, req) { ... }
|
|
123
|
+
if (val === 'on_error') {
|
|
124
|
+
return this.parseErrorHandler();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Config field: identifier: value (e.g., target: "cloudflare")
|
|
128
|
+
// Accept any identifier + colon pattern; analyzer validates the key
|
|
129
|
+
if (this.peek(1).type === TokenType.COLON && !EDGE_BINDING_KEYWORDS.has(val)) {
|
|
130
|
+
return this.parseEdgeConfigField();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// fn or async fn → regular function declaration
|
|
135
|
+
if (this.check(TokenType.FN) ||
|
|
136
|
+
(this.check(TokenType.ASYNC) && this.peek(1).type === TokenType.FN)) {
|
|
137
|
+
return this.parseStatement();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Fallback to regular statement
|
|
141
|
+
return this.parseStatement();
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
ParserClass.prototype.parseEdgeConfigField = function() {
|
|
145
|
+
const l = this.loc();
|
|
146
|
+
const key = this.advance().value; // consume identifier (e.g., 'target')
|
|
147
|
+
this.expect(TokenType.COLON, "Expected ':' after config key");
|
|
148
|
+
const value = this.parseExpression();
|
|
149
|
+
return new EdgeConfigField(key, value, l);
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
ParserClass.prototype.parseEdgeKV = function() {
|
|
153
|
+
const l = this.loc();
|
|
154
|
+
this.advance(); // consume 'kv'
|
|
155
|
+
const name = this.expect(TokenType.IDENTIFIER, "Expected KV binding name").value;
|
|
156
|
+
let config = null;
|
|
157
|
+
if (this.check(TokenType.LBRACE)) {
|
|
158
|
+
config = this._parseEdgeBindingConfig();
|
|
159
|
+
}
|
|
160
|
+
return new EdgeKVDeclaration(name, config, l);
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
ParserClass.prototype.parseEdgeSQL = function() {
|
|
164
|
+
const l = this.loc();
|
|
165
|
+
this.advance(); // consume 'sql'
|
|
166
|
+
const name = this.expect(TokenType.IDENTIFIER, "Expected SQL binding name").value;
|
|
167
|
+
let config = null;
|
|
168
|
+
if (this.check(TokenType.LBRACE)) {
|
|
169
|
+
config = this._parseEdgeBindingConfig();
|
|
170
|
+
}
|
|
171
|
+
return new EdgeSQLDeclaration(name, config, l);
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
ParserClass.prototype.parseEdgeStorage = function() {
|
|
175
|
+
const l = this.loc();
|
|
176
|
+
this.advance(); // consume 'storage'
|
|
177
|
+
const name = this.expect(TokenType.IDENTIFIER, "Expected storage binding name").value;
|
|
178
|
+
let config = null;
|
|
179
|
+
if (this.check(TokenType.LBRACE)) {
|
|
180
|
+
config = this._parseEdgeBindingConfig();
|
|
181
|
+
}
|
|
182
|
+
return new EdgeStorageDeclaration(name, config, l);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
ParserClass.prototype.parseEdgeQueue = function() {
|
|
186
|
+
const l = this.loc();
|
|
187
|
+
this.advance(); // consume 'queue'
|
|
188
|
+
const name = this.expect(TokenType.IDENTIFIER, "Expected queue binding name").value;
|
|
189
|
+
let config = null;
|
|
190
|
+
if (this.check(TokenType.LBRACE)) {
|
|
191
|
+
config = this._parseEdgeBindingConfig();
|
|
192
|
+
}
|
|
193
|
+
return new EdgeQueueDeclaration(name, config, l);
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
ParserClass.prototype.parseEdgeEnv = function() {
|
|
197
|
+
const l = this.loc();
|
|
198
|
+
this.advance(); // consume 'env'
|
|
199
|
+
const name = this.expect(TokenType.IDENTIFIER, "Expected env var name").value;
|
|
200
|
+
let defaultValue = null;
|
|
201
|
+
if (this.match(TokenType.ASSIGN)) {
|
|
202
|
+
defaultValue = this.parseExpression();
|
|
203
|
+
}
|
|
204
|
+
return new EdgeEnvDeclaration(name, defaultValue, l);
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
ParserClass.prototype.parseEdgeSecret = function() {
|
|
208
|
+
const l = this.loc();
|
|
209
|
+
this.advance(); // consume 'secret'
|
|
210
|
+
const name = this.expect(TokenType.IDENTIFIER, "Expected secret name").value;
|
|
211
|
+
return new EdgeSecretDeclaration(name, l);
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
ParserClass.prototype.parseEdgeSchedule = function() {
|
|
215
|
+
const l = this.loc();
|
|
216
|
+
this.advance(); // consume 'schedule'
|
|
217
|
+
const name = this.expect(TokenType.STRING, "Expected schedule name string").value;
|
|
218
|
+
|
|
219
|
+
// cron("expression")
|
|
220
|
+
const cronIdent = this.expect(TokenType.IDENTIFIER, "Expected 'cron' after schedule name");
|
|
221
|
+
if (cronIdent.value !== 'cron') {
|
|
222
|
+
this.error("Expected 'cron' keyword after schedule name");
|
|
223
|
+
}
|
|
224
|
+
this.expect(TokenType.LPAREN, "Expected '(' after 'cron'");
|
|
225
|
+
const cronExpr = this.expect(TokenType.STRING, "Expected cron expression string").value;
|
|
226
|
+
this.expect(TokenType.RPAREN, "Expected ')' after cron expression");
|
|
227
|
+
|
|
228
|
+
const body = this.parseBlock();
|
|
229
|
+
return new EdgeScheduleDeclaration(name, cronExpr, body, l);
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
ParserClass.prototype.parseEdgeConsume = function() {
|
|
233
|
+
const l = this.loc();
|
|
234
|
+
this.advance(); // consume 'consume'
|
|
235
|
+
const queue = this.expect(TokenType.IDENTIFIER, "Expected queue name").value;
|
|
236
|
+
|
|
237
|
+
// fn(messages) { ... } or a function reference
|
|
238
|
+
this.expect(TokenType.FN, "Expected 'fn' after queue name in consume");
|
|
239
|
+
this.expect(TokenType.LPAREN, "Expected '(' after 'fn'");
|
|
240
|
+
const params = this.parseParameterList();
|
|
241
|
+
this.expect(TokenType.RPAREN, "Expected ')' after consume parameters");
|
|
242
|
+
const body = this.parseBlock();
|
|
243
|
+
|
|
244
|
+
const handler = new AST.LambdaExpression(params, body, l);
|
|
245
|
+
return new EdgeConsumeDeclaration(queue, handler, l);
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
// Helper: parse { key: value, ... } config block for bindings
|
|
249
|
+
ParserClass.prototype._parseEdgeBindingConfig = function() {
|
|
250
|
+
this.expect(TokenType.LBRACE, "Expected '{' for binding config");
|
|
251
|
+
const config = {};
|
|
252
|
+
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
253
|
+
const key = this.expect(TokenType.IDENTIFIER, "Expected config key").value;
|
|
254
|
+
this.expect(TokenType.COLON, "Expected ':' after config key");
|
|
255
|
+
const value = this.parseExpression();
|
|
256
|
+
config[key] = value;
|
|
257
|
+
this.match(TokenType.COMMA);
|
|
258
|
+
}
|
|
259
|
+
this.expect(TokenType.RBRACE, "Expected '}' to close binding config");
|
|
260
|
+
return config;
|
|
261
|
+
};
|
|
262
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Form-specific AST Node definitions for the Tova language
|
|
2
|
+
// Extracted for lazy loading — only loaded when form { } blocks are used.
|
|
3
|
+
|
|
4
|
+
// ============================================================
|
|
5
|
+
// Form-specific nodes
|
|
6
|
+
// ============================================================
|
|
7
|
+
|
|
8
|
+
export class FormDeclaration {
|
|
9
|
+
constructor(name, typeAnnotation, fields, groups, arrays, computeds, steps, onSubmit, loc) {
|
|
10
|
+
this.type = 'FormDeclaration';
|
|
11
|
+
this.name = name;
|
|
12
|
+
this.typeAnnotation = typeAnnotation;
|
|
13
|
+
this.fields = fields;
|
|
14
|
+
this.groups = groups;
|
|
15
|
+
this.arrays = arrays;
|
|
16
|
+
this.computeds = computeds;
|
|
17
|
+
this.steps = steps;
|
|
18
|
+
this.onSubmit = onSubmit;
|
|
19
|
+
this.loc = loc;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class FormFieldDeclaration {
|
|
24
|
+
constructor(name, typeAnnotation, initialValue, validators, loc) {
|
|
25
|
+
this.type = 'FormFieldDeclaration';
|
|
26
|
+
this.name = name;
|
|
27
|
+
this.typeAnnotation = typeAnnotation;
|
|
28
|
+
this.initialValue = initialValue;
|
|
29
|
+
this.validators = validators;
|
|
30
|
+
this.loc = loc;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class FormGroupDeclaration {
|
|
35
|
+
constructor(name, condition, fields, groups, loc) {
|
|
36
|
+
this.type = 'FormGroupDeclaration';
|
|
37
|
+
this.name = name;
|
|
38
|
+
this.condition = condition;
|
|
39
|
+
this.fields = fields;
|
|
40
|
+
this.groups = groups;
|
|
41
|
+
this.loc = loc;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class FormArrayDeclaration {
|
|
46
|
+
constructor(name, fields, validators, loc) {
|
|
47
|
+
this.type = 'FormArrayDeclaration';
|
|
48
|
+
this.name = name;
|
|
49
|
+
this.fields = fields;
|
|
50
|
+
this.validators = validators;
|
|
51
|
+
this.loc = loc;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export class FormValidator {
|
|
56
|
+
constructor(name, args, isAsync, loc) {
|
|
57
|
+
this.type = 'FormValidator';
|
|
58
|
+
this.name = name;
|
|
59
|
+
this.args = args;
|
|
60
|
+
this.isAsync = isAsync;
|
|
61
|
+
this.loc = loc;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export class FormStepsDeclaration {
|
|
66
|
+
constructor(steps, loc) {
|
|
67
|
+
this.type = 'FormStepsDeclaration';
|
|
68
|
+
this.steps = steps;
|
|
69
|
+
this.loc = loc;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export class FormStep {
|
|
74
|
+
constructor(label, members, loc) {
|
|
75
|
+
this.type = 'FormStep';
|
|
76
|
+
this.label = label;
|
|
77
|
+
this.members = members;
|
|
78
|
+
this.loc = loc;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// Form-specific parser methods for the Tova language
|
|
2
|
+
// Extracted for lazy loading — only loaded when form { } blocks are encountered.
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
FormDeclaration, FormFieldDeclaration, FormGroupDeclaration,
|
|
6
|
+
FormArrayDeclaration, FormValidator, FormStepsDeclaration, FormStep,
|
|
7
|
+
} from './form-ast.js';
|
|
8
|
+
import { TokenType } from '../lexer/tokens.js';
|
|
9
|
+
|
|
10
|
+
export function installFormParser(ParserClass) {
|
|
11
|
+
if (ParserClass.prototype._formParserInstalled) return;
|
|
12
|
+
ParserClass.prototype._formParserInstalled = true;
|
|
13
|
+
|
|
14
|
+
ParserClass.prototype.parseFormDeclaration = function() {
|
|
15
|
+
const l = this.loc();
|
|
16
|
+
this.expect(TokenType.FORM);
|
|
17
|
+
const name = this.expect(TokenType.IDENTIFIER, "Expected form name").value;
|
|
18
|
+
|
|
19
|
+
let typeAnnotation = null;
|
|
20
|
+
if (this.match(TokenType.COLON)) {
|
|
21
|
+
typeAnnotation = this.parseTypeAnnotation();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
this.expect(TokenType.LBRACE, "Expected '{' after form name");
|
|
25
|
+
|
|
26
|
+
const fields = [];
|
|
27
|
+
const groups = [];
|
|
28
|
+
const arrays = [];
|
|
29
|
+
const computeds = [];
|
|
30
|
+
let steps = null;
|
|
31
|
+
let onSubmit = null;
|
|
32
|
+
|
|
33
|
+
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
34
|
+
if (this.check(TokenType.FIELD)) {
|
|
35
|
+
fields.push(this.parseFormField());
|
|
36
|
+
} else if (this.check(TokenType.GROUP)) {
|
|
37
|
+
groups.push(this.parseFormGroup());
|
|
38
|
+
} else if (this._checkFormContextual('array')) {
|
|
39
|
+
arrays.push(this.parseFormArray());
|
|
40
|
+
} else if (this.check(TokenType.COMPUTED)) {
|
|
41
|
+
computeds.push(this.parseComputed());
|
|
42
|
+
} else if (this.check(TokenType.STEPS)) {
|
|
43
|
+
steps = this.parseFormSteps();
|
|
44
|
+
} else if (this._checkFormContextual('on') && this._peekFormContextual(1, 'submit')) {
|
|
45
|
+
onSubmit = this.parseFormOnSubmit();
|
|
46
|
+
} else {
|
|
47
|
+
this.error("Expected 'field', 'group', 'array', 'computed', 'steps', or 'on submit' inside form block");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
this.expect(TokenType.RBRACE, "Expected '}' to close form block");
|
|
52
|
+
return new FormDeclaration(name, typeAnnotation, fields, groups, arrays, computeds, steps, onSubmit, l);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
ParserClass.prototype.parseFormField = function() {
|
|
56
|
+
const l = this.loc();
|
|
57
|
+
this.expect(TokenType.FIELD);
|
|
58
|
+
const name = this.expect(TokenType.IDENTIFIER, "Expected field name").value;
|
|
59
|
+
|
|
60
|
+
let typeAnnotation = null;
|
|
61
|
+
if (this.match(TokenType.COLON)) {
|
|
62
|
+
typeAnnotation = this.parseTypeAnnotation();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let initialValue = null;
|
|
66
|
+
if (this.match(TokenType.ASSIGN)) {
|
|
67
|
+
initialValue = this.parseExpression();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const validators = [];
|
|
71
|
+
if (this.check(TokenType.LBRACE)) {
|
|
72
|
+
this.advance(); // consume {
|
|
73
|
+
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
74
|
+
validators.push(this.parseFormValidator());
|
|
75
|
+
}
|
|
76
|
+
this.expect(TokenType.RBRACE, "Expected '}' to close validator block");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return new FormFieldDeclaration(name, typeAnnotation, initialValue, validators, l);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
ParserClass.prototype.parseFormValidator = function() {
|
|
83
|
+
const l = this.loc();
|
|
84
|
+
let isAsync = false;
|
|
85
|
+
if (this.check(TokenType.ASYNC)) {
|
|
86
|
+
isAsync = true;
|
|
87
|
+
this.advance();
|
|
88
|
+
}
|
|
89
|
+
const name = this.expect(TokenType.IDENTIFIER, "Expected validator name").value;
|
|
90
|
+
|
|
91
|
+
const args = [];
|
|
92
|
+
if (this.match(TokenType.LPAREN)) {
|
|
93
|
+
if (!this.check(TokenType.RPAREN)) {
|
|
94
|
+
args.push(this.parseExpression());
|
|
95
|
+
while (this.match(TokenType.COMMA)) {
|
|
96
|
+
args.push(this.parseExpression());
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
this.expect(TokenType.RPAREN, "Expected ')' after validator arguments");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return new FormValidator(name, args, isAsync, l);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
ParserClass.prototype.parseFormGroup = function() {
|
|
106
|
+
const l = this.loc();
|
|
107
|
+
this.expect(TokenType.GROUP);
|
|
108
|
+
const name = this.expect(TokenType.IDENTIFIER, "Expected group name").value;
|
|
109
|
+
|
|
110
|
+
let condition = null;
|
|
111
|
+
if (this.check(TokenType.WHEN)) {
|
|
112
|
+
this.advance(); // consume 'when'
|
|
113
|
+
condition = this.parseExpression();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
this.expect(TokenType.LBRACE, "Expected '{' after group name");
|
|
117
|
+
|
|
118
|
+
const fields = [];
|
|
119
|
+
const groups = [];
|
|
120
|
+
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
121
|
+
if (this.check(TokenType.FIELD)) {
|
|
122
|
+
fields.push(this.parseFormField());
|
|
123
|
+
} else if (this.check(TokenType.GROUP)) {
|
|
124
|
+
groups.push(this.parseFormGroup());
|
|
125
|
+
} else {
|
|
126
|
+
this.error("Expected 'field' or 'group' inside form group");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
this.expect(TokenType.RBRACE, "Expected '}' to close group block");
|
|
131
|
+
return new FormGroupDeclaration(name, condition, fields, groups, l);
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
ParserClass.prototype.parseFormArray = function() {
|
|
135
|
+
const l = this.loc();
|
|
136
|
+
this.advance(); // consume 'array' (contextual keyword — it's an IDENTIFIER with value "array")
|
|
137
|
+
const name = this.expect(TokenType.IDENTIFIER, "Expected array name").value;
|
|
138
|
+
|
|
139
|
+
this.expect(TokenType.LBRACE, "Expected '{' after array name");
|
|
140
|
+
|
|
141
|
+
const fields = [];
|
|
142
|
+
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
143
|
+
if (this.check(TokenType.FIELD)) {
|
|
144
|
+
fields.push(this.parseFormField());
|
|
145
|
+
} else {
|
|
146
|
+
this.error("Expected 'field' inside form array");
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
this.expect(TokenType.RBRACE, "Expected '}' to close array block");
|
|
151
|
+
return new FormArrayDeclaration(name, fields, [], l);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
ParserClass.prototype.parseFormSteps = function() {
|
|
155
|
+
const l = this.loc();
|
|
156
|
+
this.expect(TokenType.STEPS);
|
|
157
|
+
this.expect(TokenType.LBRACE, "Expected '{' after steps");
|
|
158
|
+
|
|
159
|
+
const stepsArr = [];
|
|
160
|
+
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
161
|
+
stepsArr.push(this.parseFormStep());
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
this.expect(TokenType.RBRACE, "Expected '}' to close steps block");
|
|
165
|
+
return new FormStepsDeclaration(stepsArr, l);
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
ParserClass.prototype.parseFormStep = function() {
|
|
169
|
+
const l = this.loc();
|
|
170
|
+
if (!this._checkFormContextual('step')) {
|
|
171
|
+
this.error("Expected 'step' inside steps block");
|
|
172
|
+
}
|
|
173
|
+
this.advance(); // consume 'step'
|
|
174
|
+
const label = this.expect(TokenType.STRING, "Expected step label string").value;
|
|
175
|
+
this.expect(TokenType.LBRACE, "Expected '{' after step label");
|
|
176
|
+
|
|
177
|
+
const members = [];
|
|
178
|
+
members.push(this.expect(TokenType.IDENTIFIER, "Expected field/group/array name").value);
|
|
179
|
+
while (this.match(TokenType.COMMA)) {
|
|
180
|
+
members.push(this.expect(TokenType.IDENTIFIER, "Expected field/group/array name").value);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
this.expect(TokenType.RBRACE, "Expected '}' to close step");
|
|
184
|
+
return new FormStep(label, members, l);
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
ParserClass.prototype.parseFormOnSubmit = function() {
|
|
188
|
+
this.advance(); // consume 'on'
|
|
189
|
+
this.advance(); // consume 'submit'
|
|
190
|
+
return this.parseBlock();
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// Helper: check if current token is an identifier with a specific value
|
|
194
|
+
if (!ParserClass.prototype._checkFormContextual) {
|
|
195
|
+
ParserClass.prototype._checkFormContextual = function(name) {
|
|
196
|
+
return this.check(TokenType.IDENTIFIER) && this.current().value === name;
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (!ParserClass.prototype._peekFormContextual) {
|
|
201
|
+
ParserClass.prototype._peekFormContextual = function(offset, name) {
|
|
202
|
+
const token = this.peek(offset);
|
|
203
|
+
return token && token.type === TokenType.IDENTIFIER && token.value === name;
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
}
|