tova 0.5.0 → 0.7.0
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 +109 -57
- package/package.json +7 -2
- package/src/analyzer/analyzer.js +315 -79
- package/src/analyzer/{client-analyzer.js → browser-analyzer.js} +20 -17
- package/src/analyzer/form-analyzer.js +113 -0
- package/src/analyzer/scope.js +2 -2
- package/src/codegen/base-codegen.js +1 -0
- package/src/codegen/{client-codegen.js → browser-codegen.js} +444 -5
- package/src/codegen/cli-codegen.js +386 -0
- package/src/codegen/codegen.js +163 -45
- 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/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 +1 -1
- package/src/parser/ast.js +45 -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/cli-ast.js +35 -0
- package/src/parser/cli-parser.js +140 -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 +86 -53
- package/src/registry/block-registry.js +56 -0
- package/src/registry/plugins/bench-plugin.js +23 -0
- package/src/registry/plugins/browser-plugin.js +30 -0
- package/src/registry/plugins/cli-plugin.js +24 -0
- package/src/registry/plugins/data-plugin.js +21 -0
- package/src/registry/plugins/edge-plugin.js +32 -0
- package/src/registry/plugins/security-plugin.js +27 -0
- package/src/registry/plugins/server-plugin.js +46 -0
- package/src/registry/plugins/shared-plugin.js +17 -0
- package/src/registry/plugins/test-plugin.js +23 -0
- package/src/registry/register-all.js +25 -0
- package/src/runtime/ssr.js +2 -2
- package/src/stdlib/inline.js +178 -3
- package/src/version.js +1 -1
|
@@ -136,6 +136,69 @@ export class ServerCodegen extends BaseCodegen {
|
|
|
136
136
|
return checks;
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
// Generate validation checks from type-level validators (Phase 3)
|
|
140
|
+
// Returns an array of JS code lines for inline validation
|
|
141
|
+
_genTypeValidatorCode(paramName, typeInfo, indent = ' ') {
|
|
142
|
+
const checks = [];
|
|
143
|
+
for (const field of typeInfo.fields) {
|
|
144
|
+
if (!field.validators || field.validators.length === 0) continue;
|
|
145
|
+
const accessor = `${paramName}.${field.name}`;
|
|
146
|
+
for (const v of field.validators) {
|
|
147
|
+
// The last argument is typically the error message
|
|
148
|
+
const msgArg = v.args.length > 0 ? this.genExpression(v.args[v.args.length - 1]) : null;
|
|
149
|
+
switch (v.name) {
|
|
150
|
+
case 'required': {
|
|
151
|
+
const msg = msgArg || `"${field.name} is required"`;
|
|
152
|
+
checks.push(`${indent}if (${accessor} === undefined || ${accessor} === null || ${accessor} === "") __validationErrors.push({ field: "${field.name}", message: ${msg} });`);
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
case 'email': {
|
|
156
|
+
const msg = msgArg || `"${field.name} must be a valid email"`;
|
|
157
|
+
checks.push(`${indent}if (${accessor} && !/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(${accessor})) __validationErrors.push({ field: "${field.name}", message: ${msg} });`);
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
case 'min': {
|
|
161
|
+
const minVal = this.genExpression(v.args[0]);
|
|
162
|
+
const minMsg = v.args.length >= 2 ? this.genExpression(v.args[1]) : `"${field.name} must be at least ${v.args[0].value || v.args[0].name}"`;
|
|
163
|
+
checks.push(`${indent}if (typeof ${accessor} === "number" && ${accessor} < ${minVal}) __validationErrors.push({ field: "${field.name}", message: ${minMsg} });`);
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
case 'max': {
|
|
167
|
+
const maxVal = this.genExpression(v.args[0]);
|
|
168
|
+
const maxMsg = v.args.length >= 2 ? this.genExpression(v.args[1]) : `"${field.name} must be at most ${v.args[0].value || v.args[0].name}"`;
|
|
169
|
+
checks.push(`${indent}if (typeof ${accessor} === "number" && ${accessor} > ${maxVal}) __validationErrors.push({ field: "${field.name}", message: ${maxMsg} });`);
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
case 'minLength': {
|
|
173
|
+
const minLen = this.genExpression(v.args[0]);
|
|
174
|
+
const minLenMsg = v.args.length >= 2 ? this.genExpression(v.args[1]) : `"${field.name} is too short"`;
|
|
175
|
+
checks.push(`${indent}if (typeof ${accessor} === "string" && ${accessor}.length < ${minLen}) __validationErrors.push({ field: "${field.name}", message: ${minLenMsg} });`);
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
case 'maxLength': {
|
|
179
|
+
const maxLen = this.genExpression(v.args[0]);
|
|
180
|
+
const maxLenMsg = v.args.length >= 2 ? this.genExpression(v.args[1]) : `"${field.name} is too long"`;
|
|
181
|
+
checks.push(`${indent}if (typeof ${accessor} === "string" && ${accessor}.length > ${maxLen}) __validationErrors.push({ field: "${field.name}", message: ${maxLenMsg} });`);
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
case 'pattern': {
|
|
185
|
+
const regex = this.genExpression(v.args[0]);
|
|
186
|
+
const patMsg = v.args.length >= 2 ? this.genExpression(v.args[1]) : `"${field.name} has invalid format"`;
|
|
187
|
+
checks.push(`${indent}if (typeof ${accessor} === "string" && !${regex}.test(${accessor})) __validationErrors.push({ field: "${field.name}", message: ${patMsg} });`);
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
case 'oneOf': {
|
|
191
|
+
const vals = this.genExpression(v.args[0]);
|
|
192
|
+
const oneOfMsg = v.args.length >= 2 ? this.genExpression(v.args[1]) : `"${field.name} has invalid value"`;
|
|
193
|
+
checks.push(`${indent}if (!${vals}.includes(${accessor})) __validationErrors.push({ field: "${field.name}", message: ${oneOfMsg} });`);
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return checks;
|
|
200
|
+
}
|
|
201
|
+
|
|
139
202
|
// Emit handler call, optionally wrapped in Promise.race for timeout
|
|
140
203
|
_emitHandlerCall(lines, callExpr, timeoutMs) {
|
|
141
204
|
if (timeoutMs) {
|
|
@@ -304,14 +367,19 @@ export class ServerCodegen extends BaseCodegen {
|
|
|
304
367
|
}
|
|
305
368
|
|
|
306
369
|
// Collect type declarations from shared blocks for model/ORM generation
|
|
307
|
-
const sharedTypes = new Map(); // typeName -> { fields: [{ name, type }] }
|
|
370
|
+
const sharedTypes = new Map(); // typeName -> { fields: [{ name, type, validators? }] }
|
|
308
371
|
const _collectTypes = (stmts) => {
|
|
309
372
|
for (const stmt of stmts) {
|
|
310
373
|
if (stmt.type === 'TypeDeclaration' && stmt.variants) {
|
|
311
374
|
const fields = [];
|
|
312
375
|
for (const v of stmt.variants) {
|
|
313
376
|
if (v.type === 'TypeField' && v.typeAnnotation) {
|
|
314
|
-
|
|
377
|
+
const fieldInfo = { name: v.name, type: v.typeAnnotation.name || (v.typeAnnotation.type === 'ArrayTypeAnnotation' ? 'Array' : 'Any') };
|
|
378
|
+
// Capture type-level validators (Phase 3) if present
|
|
379
|
+
if (v.validators && v.validators.length > 0) {
|
|
380
|
+
fieldInfo.validators = v.validators;
|
|
381
|
+
}
|
|
382
|
+
fields.push(fieldInfo);
|
|
315
383
|
}
|
|
316
384
|
}
|
|
317
385
|
if (fields.length > 0) {
|
|
@@ -2166,9 +2234,19 @@ export class ServerCodegen extends BaseCodegen {
|
|
|
2166
2234
|
lines.push(` const ${paramNames[pi]} = body.__args ? body.__args[${pi}] : body.${paramNames[pi]};`);
|
|
2167
2235
|
}
|
|
2168
2236
|
const validationChecks = this._genValidationCode(fn.params);
|
|
2169
|
-
|
|
2237
|
+
// Phase 3: Also generate type-level validator checks for typed parameters
|
|
2238
|
+
const typeValidatorChecks = [];
|
|
2239
|
+
for (const p of fn.params) {
|
|
2240
|
+
if (p.typeAnnotation && p.typeAnnotation.type === 'TypeAnnotation' && sharedTypes.has(p.typeAnnotation.name)) {
|
|
2241
|
+
const typeInfo = sharedTypes.get(p.typeAnnotation.name);
|
|
2242
|
+
const tvChecks = this._genTypeValidatorCode(p.name, typeInfo);
|
|
2243
|
+
typeValidatorChecks.push(...tvChecks);
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
const allChecks = [...validationChecks, ...typeValidatorChecks];
|
|
2247
|
+
if (allChecks.length > 0) {
|
|
2170
2248
|
lines.push(` const __validationErrors = [];`);
|
|
2171
|
-
for (const check of
|
|
2249
|
+
for (const check of allChecks) {
|
|
2172
2250
|
lines.push(check);
|
|
2173
2251
|
}
|
|
2174
2252
|
lines.push(` if (__validationErrors.length > 0) return __errorResponse(400, "VALIDATION_FAILED", "Validation failed", __validationErrors);`);
|
|
@@ -2702,6 +2780,9 @@ export class ServerCodegen extends BaseCodegen {
|
|
|
2702
2780
|
lines.push(' responses: { "200": { description: "Success" } },');
|
|
2703
2781
|
}
|
|
2704
2782
|
|
|
2783
|
+
// Close the OpenAPI path entry object
|
|
2784
|
+
lines.push('};');
|
|
2785
|
+
|
|
2705
2786
|
// Auto-generate error responses based on route context
|
|
2706
2787
|
const routeHasAuth = (route.decorators || []).some(d => d.name === 'auth');
|
|
2707
2788
|
const routeHasRateLimit = (route.decorators || []).some(d => d.name === 'rate_limit');
|
|
@@ -2709,7 +2790,8 @@ export class ServerCodegen extends BaseCodegen {
|
|
|
2709
2790
|
const routeHasTimeout = (route.decorators || []).some(d => d.name === 'timeout');
|
|
2710
2791
|
const errRef = '{ "$ref": "#/components/schemas/ErrorResponse" }';
|
|
2711
2792
|
|
|
2712
|
-
// Merge error responses into existing 200 response
|
|
2793
|
+
// Merge error responses into existing 200 response (wrapped in block scope to avoid __r redeclaration)
|
|
2794
|
+
lines.push('{');
|
|
2713
2795
|
lines.push(`const __r = __openApiSpec.paths[${JSON.stringify(path)}][${JSON.stringify(method)}].responses;`);
|
|
2714
2796
|
if (routeHasValidation || ['post', 'put', 'patch'].includes(method)) {
|
|
2715
2797
|
lines.push(`__r["400"] = { description: "Validation Failed", content: { "application/json": { schema: ${errRef} } } };`);
|
|
@@ -2733,8 +2815,7 @@ export class ServerCodegen extends BaseCodegen {
|
|
|
2733
2815
|
lines.push(`__r["504"] = { description: "Gateway Timeout", content: { "application/json": { schema: ${errRef} } } };`);
|
|
2734
2816
|
}
|
|
2735
2817
|
lines.push(`__r["500"] = { description: "Internal Server Error", content: { "application/json": { schema: ${errRef} } } };`);
|
|
2736
|
-
|
|
2737
|
-
lines.push('};');
|
|
2818
|
+
lines.push('}');
|
|
2738
2819
|
}
|
|
2739
2820
|
|
|
2740
2821
|
// Add the /docs endpoint
|
|
@@ -33,7 +33,7 @@ export const ErrorCode = {
|
|
|
33
33
|
// === Context Errors (E300–E399) ===
|
|
34
34
|
E300: { code: 'E300', title: 'Invalid context: await', category: 'context' },
|
|
35
35
|
E301: { code: 'E301', title: 'Invalid context: return', category: 'context' },
|
|
36
|
-
E302: { code: 'E302', title: 'Invalid context:
|
|
36
|
+
E302: { code: 'E302', title: 'Invalid context: browser-only', category: 'context' },
|
|
37
37
|
E303: { code: 'E303', title: 'Invalid context: server-only', category: 'context' },
|
|
38
38
|
E304: { code: 'E304', title: 'Invalid context: function-only', category: 'context' },
|
|
39
39
|
|
package/src/docs/generator.js
CHANGED
|
@@ -25,7 +25,7 @@ export class DocGenerator {
|
|
|
25
25
|
if (entry) docs.push(entry);
|
|
26
26
|
}
|
|
27
27
|
if (node.body && Array.isArray(node.body)) walk(node.body);
|
|
28
|
-
if ((node.type === 'ServerBlock' || node.type === '
|
|
28
|
+
if ((node.type === 'ServerBlock' || node.type === 'BrowserBlock' || node.type === 'SharedBlock') && node.body) {
|
|
29
29
|
walk(node.body);
|
|
30
30
|
}
|
|
31
31
|
}
|
|
@@ -31,7 +31,7 @@ export class Formatter {
|
|
|
31
31
|
|
|
32
32
|
_needsBlankLine(prevType, currType) {
|
|
33
33
|
const declTypes = ['FunctionDeclaration', 'TypeDeclaration', 'InterfaceDeclaration',
|
|
34
|
-
'ImplDeclaration', 'TraitDeclaration', 'ServerBlock', '
|
|
34
|
+
'ImplDeclaration', 'TraitDeclaration', 'ServerBlock', 'BrowserBlock', 'SharedBlock',
|
|
35
35
|
'ComponentDeclaration', 'TestBlock'];
|
|
36
36
|
return declTypes.includes(prevType) || declTypes.includes(currType);
|
|
37
37
|
}
|
|
@@ -63,7 +63,7 @@ export class Formatter {
|
|
|
63
63
|
case 'TraitDeclaration': return this.formatTraitDeclaration(node);
|
|
64
64
|
case 'DeferStatement': return this.formatDeferStatement(node);
|
|
65
65
|
case 'ServerBlock': return this.formatServerBlock(node);
|
|
66
|
-
case '
|
|
66
|
+
case 'BrowserBlock': return this.formatBrowserBlock(node);
|
|
67
67
|
case 'SharedBlock': return this.formatSharedBlock(node);
|
|
68
68
|
case 'CompoundAssignment': return `${this.i()}${this.formatExpr(node.target)} ${node.operator} ${this.formatExpr(node.value)}`;
|
|
69
69
|
case 'BlockStatement': return this.formatBlock(node);
|
|
@@ -424,9 +424,9 @@ export class Formatter {
|
|
|
424
424
|
return code;
|
|
425
425
|
}
|
|
426
426
|
|
|
427
|
-
|
|
427
|
+
formatBrowserBlock(node) {
|
|
428
428
|
const name = node.name ? ` "${node.name}"` : '';
|
|
429
|
-
let code = `${this.i()}
|
|
429
|
+
let code = `${this.i()}browser${name} {\n`;
|
|
430
430
|
this.indent++;
|
|
431
431
|
code += node.body.map(s => this.formatNode(s)).join('\n');
|
|
432
432
|
this.indent--;
|
package/src/lexer/tokens.js
CHANGED
|
@@ -85,7 +85,8 @@ export const TokenType = {
|
|
|
85
85
|
|
|
86
86
|
// Full-stack keywords
|
|
87
87
|
SERVER: 'SERVER',
|
|
88
|
-
|
|
88
|
+
BROWSER: 'BROWSER',
|
|
89
|
+
CLIENT: 'BROWSER', // deprecated alias — both map to BROWSER
|
|
89
90
|
SHARED: 'SHARED',
|
|
90
91
|
ROUTE: 'ROUTE',
|
|
91
92
|
STATE: 'STATE',
|
|
@@ -93,6 +94,10 @@ export const TokenType = {
|
|
|
93
94
|
EFFECT: 'EFFECT',
|
|
94
95
|
COMPONENT: 'COMPONENT',
|
|
95
96
|
STORE: 'STORE',
|
|
97
|
+
FORM: 'FORM',
|
|
98
|
+
FIELD: 'FIELD',
|
|
99
|
+
GROUP: 'GROUP',
|
|
100
|
+
STEPS: 'STEPS',
|
|
96
101
|
STYLE_BLOCK: 'STYLE_BLOCK',
|
|
97
102
|
|
|
98
103
|
// HTTP methods (used in route declarations)
|
|
@@ -211,7 +216,8 @@ export const Keywords = {
|
|
|
211
216
|
'is': TokenType.IS,
|
|
212
217
|
'with': TokenType.WITH,
|
|
213
218
|
'server': TokenType.SERVER,
|
|
214
|
-
'
|
|
219
|
+
'browser': TokenType.BROWSER,
|
|
220
|
+
'client': TokenType.BROWSER, // deprecated alias
|
|
215
221
|
'shared': TokenType.SHARED,
|
|
216
222
|
'route': TokenType.ROUTE,
|
|
217
223
|
'state': TokenType.STATE,
|
|
@@ -219,6 +225,10 @@ export const Keywords = {
|
|
|
219
225
|
'effect': TokenType.EFFECT,
|
|
220
226
|
'component': TokenType.COMPONENT,
|
|
221
227
|
'store': TokenType.STORE,
|
|
228
|
+
'form': TokenType.FORM,
|
|
229
|
+
'field': TokenType.FIELD,
|
|
230
|
+
'group': TokenType.GROUP,
|
|
231
|
+
'steps': TokenType.STEPS,
|
|
222
232
|
};
|
|
223
233
|
|
|
224
234
|
// Token class
|
package/src/lsp/server.js
CHANGED
|
@@ -475,7 +475,7 @@ class TovaLanguageServer {
|
|
|
475
475
|
const keywords = [
|
|
476
476
|
'fn', 'let', 'if', 'elif', 'else', 'for', 'while', 'loop', 'when', 'in',
|
|
477
477
|
'return', 'match', 'type', 'import', 'from', 'true', 'false',
|
|
478
|
-
'nil', 'server', 'client', 'shared', 'pub', 'mut',
|
|
478
|
+
'nil', 'server', 'browser', 'client', 'shared', 'pub', 'mut',
|
|
479
479
|
'try', 'catch', 'finally', 'break', 'continue', 'async', 'await',
|
|
480
480
|
'guard', 'interface', 'derive', 'route', 'model', 'db',
|
|
481
481
|
];
|
package/src/parser/ast.js
CHANGED
|
@@ -27,14 +27,16 @@ export class ServerBlock {
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
export class
|
|
30
|
+
export class BrowserBlock {
|
|
31
31
|
constructor(body, loc, name = null) {
|
|
32
|
-
this.type = '
|
|
32
|
+
this.type = 'BrowserBlock';
|
|
33
33
|
this.name = name;
|
|
34
34
|
this.body = body;
|
|
35
35
|
this.loc = loc;
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
|
+
// Deprecated alias for backward compatibility
|
|
39
|
+
export { BrowserBlock as ClientBlock };
|
|
38
40
|
|
|
39
41
|
export class SharedBlock {
|
|
40
42
|
constructor(body, loc, name = null) {
|
|
@@ -53,6 +55,24 @@ export class SecurityBlock {
|
|
|
53
55
|
}
|
|
54
56
|
}
|
|
55
57
|
|
|
58
|
+
export class CliBlock {
|
|
59
|
+
constructor(config, commands, loc) {
|
|
60
|
+
this.type = 'CliBlock';
|
|
61
|
+
this.config = config; // Array of CliConfigField
|
|
62
|
+
this.commands = commands; // Array of CliCommandDeclaration
|
|
63
|
+
this.loc = loc;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export class EdgeBlock {
|
|
68
|
+
constructor(body, loc, name = null) {
|
|
69
|
+
this.type = 'EdgeBlock';
|
|
70
|
+
this.name = name; // optional string name for named edge blocks
|
|
71
|
+
this.body = body; // Array of edge statements (config, bindings, routes, functions, etc.)
|
|
72
|
+
this.loc = loc;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
56
76
|
// ============================================================
|
|
57
77
|
// Declarations
|
|
58
78
|
// ============================================================
|
|
@@ -128,11 +148,12 @@ export class TypeVariant {
|
|
|
128
148
|
}
|
|
129
149
|
|
|
130
150
|
export class TypeField {
|
|
131
|
-
constructor(name, typeAnnotation, loc) {
|
|
151
|
+
constructor(name, typeAnnotation, loc, validators) {
|
|
132
152
|
this.type = 'TypeField';
|
|
133
153
|
this.name = name;
|
|
134
154
|
this.typeAnnotation = typeAnnotation;
|
|
135
155
|
this.loc = loc;
|
|
156
|
+
this.validators = validators || [];
|
|
136
157
|
}
|
|
137
158
|
}
|
|
138
159
|
|
|
@@ -665,7 +686,7 @@ export class RangePattern {
|
|
|
665
686
|
}
|
|
666
687
|
|
|
667
688
|
// ============================================================
|
|
668
|
-
//
|
|
689
|
+
// Browser-specific nodes (lazy-loaded from browser-ast.js, re-exported for backward compat)
|
|
669
690
|
// ============================================================
|
|
670
691
|
|
|
671
692
|
export {
|
|
@@ -673,7 +694,7 @@ export {
|
|
|
673
694
|
ComponentDeclaration, ComponentStyleBlock, StoreDeclaration,
|
|
674
695
|
JSXElement, JSXAttribute, JSXSpreadAttribute, JSXFragment,
|
|
675
696
|
JSXText, JSXExpression, JSXFor, JSXIf, JSXMatch,
|
|
676
|
-
} from './
|
|
697
|
+
} from './browser-ast.js';
|
|
677
698
|
|
|
678
699
|
// ============================================================
|
|
679
700
|
// Server-specific nodes (lazy-loaded from server-ast.js, re-exported for backward compat)
|
|
@@ -704,6 +725,25 @@ export {
|
|
|
704
725
|
SecurityTrustProxyDeclaration, SecurityHstsDeclaration,
|
|
705
726
|
} from './security-ast.js';
|
|
706
727
|
|
|
728
|
+
// ============================================================
|
|
729
|
+
// Edge-specific nodes (lazy-loaded from edge-ast.js, re-exported for backward compat)
|
|
730
|
+
// ============================================================
|
|
731
|
+
|
|
732
|
+
export {
|
|
733
|
+
EdgeConfigField, EdgeKVDeclaration, EdgeSQLDeclaration,
|
|
734
|
+
EdgeStorageDeclaration, EdgeQueueDeclaration, EdgeEnvDeclaration,
|
|
735
|
+
EdgeSecretDeclaration, EdgeScheduleDeclaration, EdgeConsumeDeclaration,
|
|
736
|
+
} from './edge-ast.js';
|
|
737
|
+
|
|
738
|
+
// ============================================================
|
|
739
|
+
// Form-specific nodes (lazy-loaded from form-ast.js, re-exported for backward compat)
|
|
740
|
+
// ============================================================
|
|
741
|
+
|
|
742
|
+
export {
|
|
743
|
+
FormDeclaration, FormFieldDeclaration, FormGroupDeclaration,
|
|
744
|
+
FormArrayDeclaration, FormValidator, FormStepsDeclaration, FormStep,
|
|
745
|
+
} from './form-ast.js';
|
|
746
|
+
|
|
707
747
|
export class TestBlock {
|
|
708
748
|
constructor(name, body, loc, options = {}) {
|
|
709
749
|
this.type = 'TestBlock';
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Extracted from ast.js for lazy loading — only loaded when
|
|
1
|
+
// Browser-specific AST Node definitions for the Tova language
|
|
2
|
+
// Extracted from ast.js for lazy loading — only loaded when browser { } blocks are used.
|
|
3
3
|
|
|
4
4
|
// ============================================================
|
|
5
|
-
//
|
|
5
|
+
// Browser-specific nodes
|
|
6
6
|
// ============================================================
|
|
7
7
|
|
|
8
8
|
export class StateDeclaration {
|
|
@@ -1,42 +1,55 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Extracted from parser.js for lazy loading — only loaded when
|
|
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
|
|
8
|
-
if (ParserClass.prototype.
|
|
9
|
-
ParserClass.prototype.
|
|
8
|
+
export function installBrowserParser(ParserClass) {
|
|
9
|
+
if (ParserClass.prototype._browserParserInstalled) return;
|
|
10
|
+
ParserClass.prototype._browserParserInstalled = true;
|
|
10
11
|
|
|
11
|
-
ParserClass
|
|
12
|
+
installFormParser(ParserClass);
|
|
13
|
+
|
|
14
|
+
ParserClass.prototype.parseBrowserBlock = function() {
|
|
12
15
|
const l = this.loc();
|
|
13
|
-
|
|
14
|
-
|
|
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 '
|
|
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.
|
|
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
|
|
31
|
-
return new AST.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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,35 @@
|
|
|
1
|
+
// CLI-specific AST Node definitions for the Tova language
|
|
2
|
+
// Extracted for lazy loading — only loaded when cli { } blocks are used.
|
|
3
|
+
|
|
4
|
+
export class CliConfigField {
|
|
5
|
+
constructor(key, value, loc) {
|
|
6
|
+
this.type = 'CliConfigField';
|
|
7
|
+
this.key = key; // string — "name", "version", "description"
|
|
8
|
+
this.value = value; // Expression (StringLiteral, etc.)
|
|
9
|
+
this.loc = loc;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class CliCommandDeclaration {
|
|
14
|
+
constructor(name, params, body, isAsync, loc) {
|
|
15
|
+
this.type = 'CliCommandDeclaration';
|
|
16
|
+
this.name = name; // string — command name
|
|
17
|
+
this.params = params; // Array of CliParam
|
|
18
|
+
this.body = body; // BlockStatement
|
|
19
|
+
this.isAsync = isAsync; // boolean
|
|
20
|
+
this.loc = loc;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class CliParam {
|
|
25
|
+
constructor(name, typeAnnotation, defaultValue, isFlag, isOptional, isRepeated, loc) {
|
|
26
|
+
this.type = 'CliParam';
|
|
27
|
+
this.name = name; // string — parameter name
|
|
28
|
+
this.typeAnnotation = typeAnnotation; // string or null — "String", "Int", "Float", "Bool"
|
|
29
|
+
this.defaultValue = defaultValue; // Expression or null
|
|
30
|
+
this.isFlag = isFlag; // boolean — prefixed with --
|
|
31
|
+
this.isOptional = isOptional; // boolean — Type? suffix
|
|
32
|
+
this.isRepeated = isRepeated; // boolean — [Type] array flag
|
|
33
|
+
this.loc = loc;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// CLI-specific parser methods for the Tova language
|
|
2
|
+
// Extracted from parser.js for lazy loading — only loaded when cli { } blocks are encountered.
|
|
3
|
+
|
|
4
|
+
import { TokenType } from '../lexer/tokens.js';
|
|
5
|
+
import * as AST from './ast.js';
|
|
6
|
+
import { CliConfigField, CliCommandDeclaration, CliParam } from './cli-ast.js';
|
|
7
|
+
|
|
8
|
+
// Valid config keys inside cli blocks
|
|
9
|
+
const CLI_CONFIG_KEYS = new Set(['name', 'version', 'description']);
|
|
10
|
+
|
|
11
|
+
export function installCliParser(ParserClass) {
|
|
12
|
+
if (ParserClass.prototype._cliParserInstalled) return;
|
|
13
|
+
ParserClass.prototype._cliParserInstalled = true;
|
|
14
|
+
|
|
15
|
+
ParserClass.prototype.parseCliBlock = function() {
|
|
16
|
+
const l = this.loc();
|
|
17
|
+
this.advance(); // consume 'cli'
|
|
18
|
+
this.expect(TokenType.LBRACE, "Expected '{' after 'cli'");
|
|
19
|
+
const config = [];
|
|
20
|
+
const commands = [];
|
|
21
|
+
|
|
22
|
+
while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
|
|
23
|
+
try {
|
|
24
|
+
// fn or async fn → command
|
|
25
|
+
if (this.check(TokenType.FN) ||
|
|
26
|
+
(this.check(TokenType.ASYNC) && this.peek(1).type === TokenType.FN)) {
|
|
27
|
+
commands.push(this.parseCliCommand());
|
|
28
|
+
}
|
|
29
|
+
// identifier followed by colon → config field
|
|
30
|
+
else if (this.check(TokenType.IDENTIFIER) && this.peek(1).type === TokenType.COLON) {
|
|
31
|
+
config.push(this.parseCliConfigField());
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
this.error("Expected config field (name: ...) or command (fn ...) inside cli block");
|
|
35
|
+
}
|
|
36
|
+
} catch (e) {
|
|
37
|
+
this.errors.push(e);
|
|
38
|
+
this._synchronizeBlock();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
this.expect(TokenType.RBRACE, "Expected '}' to close cli block");
|
|
43
|
+
return new AST.CliBlock(config, commands, l);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
ParserClass.prototype.parseCliConfigField = function() {
|
|
47
|
+
const l = this.loc();
|
|
48
|
+
const key = this.advance().value; // consume identifier
|
|
49
|
+
this.expect(TokenType.COLON, "Expected ':' after config key");
|
|
50
|
+
const value = this.parseExpression();
|
|
51
|
+
return new CliConfigField(key, value, l);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
ParserClass.prototype.parseCliCommand = function() {
|
|
55
|
+
const l = this.loc();
|
|
56
|
+
let isAsync = false;
|
|
57
|
+
if (this.check(TokenType.ASYNC)) {
|
|
58
|
+
isAsync = true;
|
|
59
|
+
this.advance(); // consume 'async'
|
|
60
|
+
}
|
|
61
|
+
this.expect(TokenType.FN, "Expected 'fn' for cli command");
|
|
62
|
+
const name = this.expect(TokenType.IDENTIFIER, "Expected command name").value;
|
|
63
|
+
this.expect(TokenType.LPAREN, "Expected '(' after command name");
|
|
64
|
+
const params = this.parseCliParameterList();
|
|
65
|
+
this.expect(TokenType.RPAREN, "Expected ')' after parameters");
|
|
66
|
+
|
|
67
|
+
// Parse body
|
|
68
|
+
const body = this.parseBlock();
|
|
69
|
+
|
|
70
|
+
return new CliCommandDeclaration(name, params, body, isAsync, l);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
ParserClass.prototype.parseCliParameterList = function() {
|
|
74
|
+
const params = [];
|
|
75
|
+
while (!this.check(TokenType.RPAREN) && !this.isAtEnd()) {
|
|
76
|
+
if (params.length > 0) {
|
|
77
|
+
this.expect(TokenType.COMMA, "Expected ',' between parameters");
|
|
78
|
+
}
|
|
79
|
+
params.push(this.parseCliParam());
|
|
80
|
+
}
|
|
81
|
+
return params;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
ParserClass.prototype.parseCliParam = function() {
|
|
85
|
+
const l = this.loc();
|
|
86
|
+
let isFlag = false;
|
|
87
|
+
|
|
88
|
+
// Check for -- prefix (two MINUS tokens)
|
|
89
|
+
if (this.check(TokenType.MINUS) && this.peek(1).type === TokenType.MINUS) {
|
|
90
|
+
isFlag = true;
|
|
91
|
+
this.advance(); // consume first -
|
|
92
|
+
this.advance(); // consume second -
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const name = this.expect(TokenType.IDENTIFIER, "Expected parameter name").value;
|
|
96
|
+
|
|
97
|
+
// Parse optional type annotation: param: Type, param: Type?, param: [Type]
|
|
98
|
+
let typeAnnotation = null;
|
|
99
|
+
let isOptional = false;
|
|
100
|
+
let isRepeated = false;
|
|
101
|
+
|
|
102
|
+
if (this.match(TokenType.COLON)) {
|
|
103
|
+
// [Type] → repeated
|
|
104
|
+
if (this.check(TokenType.LBRACKET)) {
|
|
105
|
+
isRepeated = true;
|
|
106
|
+
this.advance(); // consume [
|
|
107
|
+
typeAnnotation = this._parseCliTypeName();
|
|
108
|
+
this.expect(TokenType.RBRACKET, "Expected ']' after array type");
|
|
109
|
+
} else {
|
|
110
|
+
typeAnnotation = this._parseCliTypeName();
|
|
111
|
+
// Type? → optional
|
|
112
|
+
if (this.check(TokenType.QUESTION)) {
|
|
113
|
+
isOptional = true;
|
|
114
|
+
this.advance(); // consume ?
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Bool flags are implicitly optional (default false)
|
|
120
|
+
if (isFlag && typeAnnotation === 'Bool' && !isOptional) {
|
|
121
|
+
isOptional = true;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Parse default value: = expr
|
|
125
|
+
let defaultValue = null;
|
|
126
|
+
if (this.match(TokenType.ASSIGN)) {
|
|
127
|
+
defaultValue = this.parseExpression();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return new CliParam(name, typeAnnotation, defaultValue, isFlag, isOptional, isRepeated, l);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// Helper to parse a simple type name (String, Int, Float, Bool, or IDENTIFIER)
|
|
134
|
+
ParserClass.prototype._parseCliTypeName = function() {
|
|
135
|
+
if (this.check(TokenType.IDENTIFIER) || this.check(TokenType.TYPE)) {
|
|
136
|
+
return this.advance().value;
|
|
137
|
+
}
|
|
138
|
+
this.error("Expected type name");
|
|
139
|
+
};
|
|
140
|
+
}
|