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,553 @@
|
|
|
1
|
+
// Form codegen helper functions for the Tova language
|
|
2
|
+
// Generates the revealing-module IIFE pattern for form { } blocks.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generate the validator function body for a single field.
|
|
6
|
+
* @param {string} fieldName - The field name (e.g., "email")
|
|
7
|
+
* @param {Array} validators - Array of FormValidator AST nodes
|
|
8
|
+
* @param {Function} genExpression - The codegen's genExpression method (bound)
|
|
9
|
+
* @param {string} indent - Current indentation string
|
|
10
|
+
* @returns {string} The complete validator function source
|
|
11
|
+
*/
|
|
12
|
+
export function generateValidatorFn(fieldName, validators, genExpression, indent) {
|
|
13
|
+
if (!validators || validators.length === 0) {
|
|
14
|
+
return `${indent}function __validate_${fieldName}(v) { return null; }\n`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const lines = [];
|
|
18
|
+
lines.push(`${indent}function __validate_${fieldName}(v) {`);
|
|
19
|
+
|
|
20
|
+
for (const v of validators) {
|
|
21
|
+
const msg = v.args.length > 0 ? genExpression(v.args[v.args.length - 1]) : '"Validation failed"';
|
|
22
|
+
|
|
23
|
+
switch (v.name) {
|
|
24
|
+
case 'required':
|
|
25
|
+
lines.push(`${indent} if (v === undefined || v === null || v === "") return ${msg};`);
|
|
26
|
+
break;
|
|
27
|
+
|
|
28
|
+
case 'minLength': {
|
|
29
|
+
const len = v.args.length >= 2 ? genExpression(v.args[0]) : '0';
|
|
30
|
+
lines.push(`${indent} if (typeof v === "string" && v.length < ${len}) return ${msg};`);
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
case 'maxLength': {
|
|
35
|
+
const len = v.args.length >= 2 ? genExpression(v.args[0]) : 'Infinity';
|
|
36
|
+
lines.push(`${indent} if (typeof v === "string" && v.length > ${len}) return ${msg};`);
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
case 'min': {
|
|
41
|
+
const threshold = v.args.length >= 2 ? genExpression(v.args[0]) : '0';
|
|
42
|
+
lines.push(`${indent} if (typeof v === "number" && v < ${threshold}) return ${msg};`);
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
case 'max': {
|
|
47
|
+
const threshold = v.args.length >= 2 ? genExpression(v.args[0]) : 'Infinity';
|
|
48
|
+
lines.push(`${indent} if (typeof v === "number" && v > ${threshold}) return ${msg};`);
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
case 'pattern': {
|
|
53
|
+
const regex = v.args.length >= 2 ? genExpression(v.args[0]) : '/./';
|
|
54
|
+
lines.push(`${indent} if (typeof v === "string" && !${regex}.test(v)) return ${msg};`);
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
case 'email':
|
|
59
|
+
lines.push(`${indent} if (typeof v === "string" && !/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(v)) return ${msg};`);
|
|
60
|
+
break;
|
|
61
|
+
|
|
62
|
+
case 'matches': {
|
|
63
|
+
const siblingField = v.args[0];
|
|
64
|
+
const siblingName = siblingField && (siblingField.name || siblingField);
|
|
65
|
+
const matchMsg = v.args.length >= 2 ? genExpression(v.args[1]) : '"Fields do not match"';
|
|
66
|
+
if (siblingName) {
|
|
67
|
+
lines.push(`${indent} if (v !== __${siblingName}_value()) return ${matchMsg};`);
|
|
68
|
+
}
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
case 'validate': {
|
|
73
|
+
// Custom validator: validate(fn(v) ...) — the first arg is a lambda/function
|
|
74
|
+
const fn = v.args.length > 0 ? genExpression(v.args[0]) : '(() => null)';
|
|
75
|
+
if (v.isAsync) {
|
|
76
|
+
// Async validators are handled in a later task — emit a comment placeholder
|
|
77
|
+
lines.push(`${indent} // async validate: ${fn} (deferred to async validation)`);
|
|
78
|
+
} else {
|
|
79
|
+
lines.push(`${indent} { const __r = ${fn}(v); if (__r) return __r; }`);
|
|
80
|
+
}
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
default:
|
|
85
|
+
// Unknown validator — emit as custom function call for extensibility
|
|
86
|
+
if (v.args.length > 0) {
|
|
87
|
+
const allArgs = v.args.map(a => genExpression(a)).join(', ');
|
|
88
|
+
lines.push(`${indent} // custom validator: ${v.name}(${allArgs})`);
|
|
89
|
+
}
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
lines.push(`${indent} return null;`);
|
|
95
|
+
lines.push(`${indent}}`);
|
|
96
|
+
return lines.join('\n') + '\n';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Generate the three signal pairs + initial const for a field.
|
|
101
|
+
* @param {string} fieldName - The field name
|
|
102
|
+
* @param {string} initialExpr - The generated JS expression for the initial value
|
|
103
|
+
* @param {string} indent - Current indentation string
|
|
104
|
+
* @returns {string} The signal declarations
|
|
105
|
+
*/
|
|
106
|
+
export function generateFieldSignals(fieldName, initialExpr, indent) {
|
|
107
|
+
const lines = [];
|
|
108
|
+
lines.push(`${indent}const __${fieldName}_initial = ${initialExpr};`);
|
|
109
|
+
lines.push(`${indent}const [__${fieldName}_value, __set_${fieldName}_value] = createSignal(${initialExpr});`);
|
|
110
|
+
lines.push(`${indent}const [__${fieldName}_error, __set_${fieldName}_error] = createSignal(null);`);
|
|
111
|
+
lines.push(`${indent}const [__${fieldName}_touched, __set_${fieldName}_touched] = createSignal(false);`);
|
|
112
|
+
return lines.join('\n') + '\n';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Generate the field accessor object for a single field.
|
|
117
|
+
* @param {string} fieldName - The field name (may include prefix, e.g., "shipping_street")
|
|
118
|
+
* @param {string} indent - Current indentation string
|
|
119
|
+
* @returns {string} The field accessor object source
|
|
120
|
+
*/
|
|
121
|
+
export function generateFieldAccessor(fieldName, indent) {
|
|
122
|
+
const lines = [];
|
|
123
|
+
lines.push(`${indent}const ${fieldName} = {`);
|
|
124
|
+
lines.push(`${indent} get value() { return __${fieldName}_value(); },`);
|
|
125
|
+
lines.push(`${indent} get error() { return __${fieldName}_error(); },`);
|
|
126
|
+
lines.push(`${indent} get touched() { return __${fieldName}_touched(); },`);
|
|
127
|
+
lines.push(`${indent} set(v) { __set_${fieldName}_value(v); if (__${fieldName}_touched()) __set_${fieldName}_error(__validate_${fieldName}(v)); },`);
|
|
128
|
+
lines.push(`${indent} blur() { __set_${fieldName}_touched(true); __set_${fieldName}_error(__validate_${fieldName}(__${fieldName}_value())); },`);
|
|
129
|
+
lines.push(`${indent} validate() { const e = __validate_${fieldName}(__${fieldName}_value()); __set_${fieldName}_error(e); return e === null; },`);
|
|
130
|
+
lines.push(`${indent} reset() { __set_${fieldName}_value(__${fieldName}_initial); __set_${fieldName}_error(null); __set_${fieldName}_touched(false); },`);
|
|
131
|
+
lines.push(`${indent}};`);
|
|
132
|
+
return lines.join('\n') + '\n';
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Generate a conditional guard line for a validator function.
|
|
137
|
+
* When the group condition is false, skip validation (return null).
|
|
138
|
+
* @param {string|null} conditionGuardExpr - JS expression that, when true, means "skip validation"
|
|
139
|
+
* @param {string} indent - Current indentation string
|
|
140
|
+
* @returns {string} The guard line, or empty string if no condition
|
|
141
|
+
*/
|
|
142
|
+
export function generateConditionGuard(conditionGuardExpr, indent) {
|
|
143
|
+
if (!conditionGuardExpr) return '';
|
|
144
|
+
return `${indent} if (${conditionGuardExpr}) return null;\n`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Generate the validator function with an optional condition guard for groups.
|
|
149
|
+
* @param {string} fieldName - The prefixed field name (e.g., "shipping_street")
|
|
150
|
+
* @param {Array} validators - Array of FormValidator AST nodes
|
|
151
|
+
* @param {Function} genExpression - The codegen's genExpression method (bound)
|
|
152
|
+
* @param {string} indent - Current indentation string
|
|
153
|
+
* @param {string|null} conditionGuardExpr - If non-null, JS expression to guard (skip when truthy)
|
|
154
|
+
* @returns {string} The complete validator function source
|
|
155
|
+
*/
|
|
156
|
+
export function generateGuardedValidatorFn(fieldName, validators, genExpression, indent, conditionGuardExpr) {
|
|
157
|
+
if (!validators || validators.length === 0) {
|
|
158
|
+
if (conditionGuardExpr) {
|
|
159
|
+
const lines = [];
|
|
160
|
+
lines.push(`${indent}function __validate_${fieldName}(v) {`);
|
|
161
|
+
lines.push(`${indent} if (${conditionGuardExpr}) return null;`);
|
|
162
|
+
lines.push(`${indent} return null;`);
|
|
163
|
+
lines.push(`${indent}}`);
|
|
164
|
+
return lines.join('\n') + '\n';
|
|
165
|
+
}
|
|
166
|
+
return `${indent}function __validate_${fieldName}(v) { return null; }\n`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const lines = [];
|
|
170
|
+
lines.push(`${indent}function __validate_${fieldName}(v) {`);
|
|
171
|
+
|
|
172
|
+
// Insert condition guard before validators
|
|
173
|
+
if (conditionGuardExpr) {
|
|
174
|
+
lines.push(`${indent} if (${conditionGuardExpr}) return null;`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
for (const v of validators) {
|
|
178
|
+
const msg = v.args.length > 0 ? genExpression(v.args[v.args.length - 1]) : '"Validation failed"';
|
|
179
|
+
|
|
180
|
+
switch (v.name) {
|
|
181
|
+
case 'required':
|
|
182
|
+
lines.push(`${indent} if (v === undefined || v === null || v === "") return ${msg};`);
|
|
183
|
+
break;
|
|
184
|
+
|
|
185
|
+
case 'minLength': {
|
|
186
|
+
const len = v.args.length >= 2 ? genExpression(v.args[0]) : '0';
|
|
187
|
+
lines.push(`${indent} if (typeof v === "string" && v.length < ${len}) return ${msg};`);
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
case 'maxLength': {
|
|
192
|
+
const len = v.args.length >= 2 ? genExpression(v.args[0]) : 'Infinity';
|
|
193
|
+
lines.push(`${indent} if (typeof v === "string" && v.length > ${len}) return ${msg};`);
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
case 'min': {
|
|
198
|
+
const threshold = v.args.length >= 2 ? genExpression(v.args[0]) : '0';
|
|
199
|
+
lines.push(`${indent} if (typeof v === "number" && v < ${threshold}) return ${msg};`);
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
case 'max': {
|
|
204
|
+
const threshold = v.args.length >= 2 ? genExpression(v.args[0]) : 'Infinity';
|
|
205
|
+
lines.push(`${indent} if (typeof v === "number" && v > ${threshold}) return ${msg};`);
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
case 'pattern': {
|
|
210
|
+
const regex = v.args.length >= 2 ? genExpression(v.args[0]) : '/./';
|
|
211
|
+
lines.push(`${indent} if (typeof v === "string" && !${regex}.test(v)) return ${msg};`);
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
case 'email':
|
|
216
|
+
lines.push(`${indent} if (typeof v === "string" && !/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(v)) return ${msg};`);
|
|
217
|
+
break;
|
|
218
|
+
|
|
219
|
+
case 'matches': {
|
|
220
|
+
const siblingField = v.args[0];
|
|
221
|
+
const siblingName = siblingField && (siblingField.name || siblingField);
|
|
222
|
+
const matchMsg = v.args.length >= 2 ? genExpression(v.args[1]) : '"Fields do not match"';
|
|
223
|
+
if (siblingName) {
|
|
224
|
+
lines.push(`${indent} if (v !== __${siblingName}_value()) return ${matchMsg};`);
|
|
225
|
+
}
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
case 'validate': {
|
|
230
|
+
const fn = v.args.length > 0 ? genExpression(v.args[0]) : '(() => null)';
|
|
231
|
+
if (v.isAsync) {
|
|
232
|
+
lines.push(`${indent} // async validate: ${fn} (deferred to async validation)`);
|
|
233
|
+
} else {
|
|
234
|
+
lines.push(`${indent} { const __r = ${fn}(v); if (__r) return __r; }`);
|
|
235
|
+
}
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
default:
|
|
240
|
+
if (v.args.length > 0) {
|
|
241
|
+
const allArgs = v.args.map(a => genExpression(a)).join(', ');
|
|
242
|
+
lines.push(`${indent} // custom validator: ${v.name}(${allArgs})`);
|
|
243
|
+
}
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
lines.push(`${indent} return null;`);
|
|
249
|
+
lines.push(`${indent}}`);
|
|
250
|
+
return lines.join('\n') + '\n';
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Generate the group accessor object.
|
|
255
|
+
* @param {string} groupName - The group name (e.g., "shipping")
|
|
256
|
+
* @param {Array<{name: string, prefixedName: string}>} childFields - Fields in this group with their prefixed signal names
|
|
257
|
+
* @param {Array<string>} childGroupNames - Names of nested sub-group accessors
|
|
258
|
+
* @param {string} indent - Current indentation string
|
|
259
|
+
* @returns {string} The group accessor object source
|
|
260
|
+
*/
|
|
261
|
+
export function generateGroupAccessor(groupName, childFields, childGroupNames, indent) {
|
|
262
|
+
const lines = [];
|
|
263
|
+
lines.push(`${indent}const ${groupName} = {`);
|
|
264
|
+
|
|
265
|
+
// Named field accessors as properties
|
|
266
|
+
for (const f of childFields) {
|
|
267
|
+
lines.push(`${indent} ${f.name}: ${f.prefixedName},`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Nested group accessors as properties (reference the group's local variable name)
|
|
271
|
+
for (const g of childGroupNames) {
|
|
272
|
+
lines.push(`${indent} ${g.localName}: ${g.localName},`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// get values() — returns object with field values
|
|
276
|
+
const valuesEntries = childFields.map(f => `${f.name}: __${f.prefixedName}_value()`).join(', ');
|
|
277
|
+
lines.push(`${indent} get values() { return { ${valuesEntries} }; },`);
|
|
278
|
+
|
|
279
|
+
// get isValid() — true when all child fields have null errors
|
|
280
|
+
const isValidParts = childFields.map(f => `__${f.prefixedName}_error() === null`);
|
|
281
|
+
const isValidExpr = isValidParts.length > 0 ? isValidParts.join(' && ') : 'true';
|
|
282
|
+
lines.push(`${indent} get isValid() { return ${isValidExpr}; },`);
|
|
283
|
+
|
|
284
|
+
// get isDirty() — true when any child field differs from initial
|
|
285
|
+
const isDirtyParts = childFields.map(f => `__${f.prefixedName}_value() !== __${f.prefixedName}_initial`);
|
|
286
|
+
const isDirtyExpr = isDirtyParts.length > 0 ? isDirtyParts.join(' || ') : 'false';
|
|
287
|
+
lines.push(`${indent} get isDirty() { return ${isDirtyExpr}; },`);
|
|
288
|
+
|
|
289
|
+
// reset() — resets all child fields
|
|
290
|
+
const resetCalls = childFields.map(f => `${f.prefixedName}.reset()`).join('; ');
|
|
291
|
+
lines.push(`${indent} reset() { ${resetCalls}; },`);
|
|
292
|
+
|
|
293
|
+
lines.push(`${indent}};`);
|
|
294
|
+
return lines.join('\n') + '\n';
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Generate a condition expression where form field identifiers are replaced
|
|
299
|
+
* with their signal value calls (__fieldName_value()).
|
|
300
|
+
* @param {Object} condNode - The condition AST node
|
|
301
|
+
* @param {Function} genExpression - The codegen's genExpression method (bound)
|
|
302
|
+
* @param {Set<string>} formFieldNames - Set of all known form field names (top-level)
|
|
303
|
+
* @returns {string} The JS expression with field references replaced by signal calls
|
|
304
|
+
*/
|
|
305
|
+
export function generateConditionExpr(condNode, genExpression, formFieldNames) {
|
|
306
|
+
if (!condNode) return 'true';
|
|
307
|
+
|
|
308
|
+
// For identifier nodes that reference form fields, generate signal value calls
|
|
309
|
+
if (condNode.type === 'Identifier' && formFieldNames.has(condNode.name)) {
|
|
310
|
+
return `__${condNode.name}_value()`;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// For unary not: recurse into the operand
|
|
314
|
+
if (condNode.type === 'UnaryExpression' && (condNode.operator === 'not' || condNode.operator === '!')) {
|
|
315
|
+
const operand = generateConditionExpr(condNode.operand, genExpression, formFieldNames);
|
|
316
|
+
return `(!${operand})`;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// For binary/logical expressions: recurse into both sides
|
|
320
|
+
if (condNode.type === 'BinaryExpression' || condNode.type === 'LogicalExpression') {
|
|
321
|
+
const left = generateConditionExpr(condNode.left, genExpression, formFieldNames);
|
|
322
|
+
const right = generateConditionExpr(condNode.right, genExpression, formFieldNames);
|
|
323
|
+
const op = condNode.operator === 'and' ? '&&' : condNode.operator === 'or' ? '||' : condNode.operator;
|
|
324
|
+
return `(${left} ${op} ${right})`;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Fallback: use the standard genExpression
|
|
328
|
+
return genExpression(condNode);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Recursively generate all code for a form group (signals, validators, accessors, group accessor).
|
|
333
|
+
* Collects all prefixed field names into the provided allFields array for form-level computeds.
|
|
334
|
+
* @param {Object} group - FormGroupDeclaration AST node
|
|
335
|
+
* @param {string} prefix - Current prefix (e.g., "shipping_" or "billing_address_")
|
|
336
|
+
* @param {Function} genExpression - The codegen's genExpression method (bound)
|
|
337
|
+
* @param {string} indent - Current indentation string
|
|
338
|
+
* @param {Array<string>} allPrefixedNames - Accumulator for all prefixed field names (for form-level isValid/isDirty)
|
|
339
|
+
* @param {string|null} conditionGuardExpr - JS expression to guard validators (from conditional group)
|
|
340
|
+
* @param {Array<{groupName: string, condExpr: string|null}>} conditionalGroups - Accumulator for conditional group info
|
|
341
|
+
* @param {Set<string>} formFieldNames - Set of all top-level form field names (for condition expression resolution)
|
|
342
|
+
* @returns {string} The complete generated code for this group
|
|
343
|
+
*/
|
|
344
|
+
export function generateGroupCode(group, prefix, genExpression, indent, allPrefixedNames, conditionGuardExpr, conditionalGroups, formFieldNames) {
|
|
345
|
+
const p = [];
|
|
346
|
+
const groupPrefix = prefix + group.name + '_';
|
|
347
|
+
|
|
348
|
+
// Determine condition guard for this group's validators
|
|
349
|
+
let guardExpr = conditionGuardExpr || null;
|
|
350
|
+
if (group.condition) {
|
|
351
|
+
// Generate the condition expression with field references resolved to signal value calls
|
|
352
|
+
const condExpr = generateConditionExpr(group.condition, genExpression, formFieldNames || new Set());
|
|
353
|
+
// Guard: when condition is false, skip validation
|
|
354
|
+
const thisGuard = `!(${condExpr})`;
|
|
355
|
+
// Combine with parent guard if any
|
|
356
|
+
guardExpr = conditionGuardExpr ? `${conditionGuardExpr} || ${thisGuard}` : thisGuard;
|
|
357
|
+
|
|
358
|
+
// Track this conditional group for form-level isValid
|
|
359
|
+
conditionalGroups.push({ groupPrefix, condExpr });
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Child field info for the group accessor
|
|
363
|
+
const childFields = [];
|
|
364
|
+
|
|
365
|
+
// Generate signals, validators, and accessors for each field
|
|
366
|
+
for (const field of group.fields) {
|
|
367
|
+
const prefixedName = groupPrefix + field.name;
|
|
368
|
+
const init = field.initialValue ? genExpression(field.initialValue) : 'null';
|
|
369
|
+
|
|
370
|
+
p.push(generateFieldSignals(prefixedName, init, indent));
|
|
371
|
+
p.push(generateGuardedValidatorFn(prefixedName, field.validators, genExpression, indent, guardExpr));
|
|
372
|
+
p.push(generateFieldAccessor(prefixedName, indent));
|
|
373
|
+
|
|
374
|
+
allPrefixedNames.push(prefixedName);
|
|
375
|
+
childFields.push({ name: field.name, prefixedName });
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Recurse into nested groups
|
|
379
|
+
const childGroupNames = [];
|
|
380
|
+
for (const subGroup of (group.groups || [])) {
|
|
381
|
+
p.push(generateGroupCode(subGroup, groupPrefix, genExpression, indent, allPrefixedNames, guardExpr, conditionalGroups, formFieldNames));
|
|
382
|
+
childGroupNames.push({ localName: subGroup.name, prefixedName: groupPrefix + subGroup.name });
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Generate the group accessor object
|
|
386
|
+
p.push(generateGroupAccessor(group.name, childFields, childGroupNames, indent));
|
|
387
|
+
|
|
388
|
+
return p.join('');
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Generate the complete code for a form array declaration.
|
|
393
|
+
* Produces:
|
|
394
|
+
* - Items list signal (createSignal([]))
|
|
395
|
+
* - Auto-increment ID counter
|
|
396
|
+
* - Item factory function (with signal-backed fields, validators, accessors)
|
|
397
|
+
* - Array accessor object (items, length, add, remove, move)
|
|
398
|
+
*
|
|
399
|
+
* @param {Object} arrayDecl - FormArrayDeclaration AST node
|
|
400
|
+
* @param {Function} genExpression - The codegen's genExpression method (bound)
|
|
401
|
+
* @param {string} indent - Current indentation string
|
|
402
|
+
* @returns {string} The complete generated code for this array
|
|
403
|
+
*/
|
|
404
|
+
export function generateArrayCode(arrayDecl, genExpression, indent) {
|
|
405
|
+
const name = arrayDecl.name;
|
|
406
|
+
const fields = arrayDecl.fields || [];
|
|
407
|
+
const p = [];
|
|
408
|
+
|
|
409
|
+
// Items list signal
|
|
410
|
+
p.push(`${indent}const [__${name}, __set_${name}] = createSignal([]);\n`);
|
|
411
|
+
p.push(`${indent}let __${name}_nextId = 0;\n\n`);
|
|
412
|
+
|
|
413
|
+
// Item factory function
|
|
414
|
+
p.push(`${indent}function __create${capitalize(name)}Item(defaults) {\n`);
|
|
415
|
+
const fi = indent + ' '; // factory indent
|
|
416
|
+
|
|
417
|
+
p.push(`${fi}const __id = __${name}_nextId++;\n`);
|
|
418
|
+
|
|
419
|
+
// For each field: initial value, signals, validator, accessor
|
|
420
|
+
for (const field of fields) {
|
|
421
|
+
const fname = field.name;
|
|
422
|
+
const defaultVal = field.initialValue ? genExpression(field.initialValue) : 'null';
|
|
423
|
+
|
|
424
|
+
// Initial value (from defaults param or field default)
|
|
425
|
+
p.push(`${fi}const __${fname}_initial = (defaults && defaults.${fname} !== undefined) ? defaults.${fname} : ${defaultVal};\n`);
|
|
426
|
+
|
|
427
|
+
// Signals
|
|
428
|
+
p.push(`${fi}const [__${fname}_value, __set_${fname}_value] = createSignal(__${fname}_initial);\n`);
|
|
429
|
+
p.push(`${fi}const [__${fname}_error, __set_${fname}_error] = createSignal(null);\n`);
|
|
430
|
+
p.push(`${fi}const [__${fname}_touched, __set_${fname}_touched] = createSignal(false);\n`);
|
|
431
|
+
p.push(`\n`);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Validator functions for each field
|
|
435
|
+
for (const field of fields) {
|
|
436
|
+
p.push(generateValidatorFn(field.name, field.validators, genExpression, fi));
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Return the item object with field accessors
|
|
440
|
+
p.push(`${fi}return {\n`);
|
|
441
|
+
const ri = fi + ' '; // return indent
|
|
442
|
+
|
|
443
|
+
p.push(`${ri}__id,\n`);
|
|
444
|
+
|
|
445
|
+
for (const field of fields) {
|
|
446
|
+
const fname = field.name;
|
|
447
|
+
p.push(`${ri}${fname}: {\n`);
|
|
448
|
+
const ai = ri + ' '; // accessor indent
|
|
449
|
+
p.push(`${ai}get value() { return __${fname}_value(); },\n`);
|
|
450
|
+
p.push(`${ai}get error() { return __${fname}_error(); },\n`);
|
|
451
|
+
p.push(`${ai}get touched() { return __${fname}_touched(); },\n`);
|
|
452
|
+
p.push(`${ai}set(v) { __set_${fname}_value(v); if (__${fname}_touched()) __set_${fname}_error(__validate_${fname}(v)); },\n`);
|
|
453
|
+
p.push(`${ai}blur() { __set_${fname}_touched(true); __set_${fname}_error(__validate_${fname}(__${fname}_value())); },\n`);
|
|
454
|
+
p.push(`${ai}validate() { const e = __validate_${fname}(__${fname}_value()); __set_${fname}_error(e); return e === null; },\n`);
|
|
455
|
+
p.push(`${ai}reset() { __set_${fname}_value(__${fname}_initial); __set_${fname}_error(null); __set_${fname}_touched(false); },\n`);
|
|
456
|
+
p.push(`${ri}},\n`);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Item-level values getter
|
|
460
|
+
const valuesEntries = fields.map(f => `${f.name}: __${f.name}_value()`).join(', ');
|
|
461
|
+
p.push(`${ri}get values() { return { ${valuesEntries} }; },\n`);
|
|
462
|
+
|
|
463
|
+
// Item-level isValid getter
|
|
464
|
+
const isValidParts = fields.map(f => `__${f.name}_error() === null`);
|
|
465
|
+
const isValidExpr = isValidParts.length > 0 ? isValidParts.join(' && ') : 'true';
|
|
466
|
+
p.push(`${ri}get isValid() { return ${isValidExpr}; },\n`);
|
|
467
|
+
|
|
468
|
+
p.push(`${fi}};\n`);
|
|
469
|
+
p.push(`${indent}}\n\n`);
|
|
470
|
+
|
|
471
|
+
// Array accessor object
|
|
472
|
+
p.push(`${indent}const ${name} = {\n`);
|
|
473
|
+
const oi = indent + ' '; // object indent
|
|
474
|
+
|
|
475
|
+
p.push(`${oi}get items() { return __${name}(); },\n`);
|
|
476
|
+
p.push(`${oi}get length() { return __${name}().length; },\n`);
|
|
477
|
+
|
|
478
|
+
// add(defaults)
|
|
479
|
+
p.push(`${oi}add(defaults) {\n`);
|
|
480
|
+
p.push(`${oi} const item = __create${capitalize(name)}Item(defaults);\n`);
|
|
481
|
+
p.push(`${oi} __set_${name}(prev => [...prev, item]);\n`);
|
|
482
|
+
p.push(`${oi} return item;\n`);
|
|
483
|
+
p.push(`${oi}},\n`);
|
|
484
|
+
|
|
485
|
+
// remove(item)
|
|
486
|
+
p.push(`${oi}remove(item) {\n`);
|
|
487
|
+
p.push(`${oi} __set_${name}(prev => prev.filter(i => i.__id !== item.__id));\n`);
|
|
488
|
+
p.push(`${oi}},\n`);
|
|
489
|
+
|
|
490
|
+
// move(from, to)
|
|
491
|
+
p.push(`${oi}move(from, to) {\n`);
|
|
492
|
+
p.push(`${oi} __set_${name}(prev => {\n`);
|
|
493
|
+
p.push(`${oi} const arr = [...prev];\n`);
|
|
494
|
+
p.push(`${oi} const [moved] = arr.splice(from, 1);\n`);
|
|
495
|
+
p.push(`${oi} arr.splice(to, 0, moved);\n`);
|
|
496
|
+
p.push(`${oi} return arr;\n`);
|
|
497
|
+
p.push(`${oi} });\n`);
|
|
498
|
+
p.push(`${oi}},\n`);
|
|
499
|
+
|
|
500
|
+
p.push(`${indent}};\n`);
|
|
501
|
+
|
|
502
|
+
return p.join('');
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Generate a createEffect that debounces an async validator for a form field.
|
|
507
|
+
* The effect:
|
|
508
|
+
* 1. Watches the field's value signal
|
|
509
|
+
* 2. Debounces with setTimeout (300ms)
|
|
510
|
+
* 3. Runs the async validation function
|
|
511
|
+
* 4. Uses a version counter to discard stale results
|
|
512
|
+
* 5. Sets the error signal with the result
|
|
513
|
+
*
|
|
514
|
+
* @param {string} fieldName - The field name (e.g., "email")
|
|
515
|
+
* @param {Object} asyncValidator - The FormValidator AST node with isAsync: true
|
|
516
|
+
* @param {Function} genExpression - The codegen's genExpression method (bound)
|
|
517
|
+
* @param {string} indent - Current indentation string
|
|
518
|
+
* @returns {string} The complete async validator effect source
|
|
519
|
+
*/
|
|
520
|
+
export function generateAsyncValidatorEffect(fieldName, asyncValidator, genExpression, indent = ' ') {
|
|
521
|
+
const fn = genExpression(asyncValidator.args[0]); // The async validation function
|
|
522
|
+
const lines = [];
|
|
523
|
+
lines.push(`${indent}let __${fieldName}_asyncVersion = 0;`);
|
|
524
|
+
lines.push(`${indent}let __${fieldName}_asyncTimer = null;`);
|
|
525
|
+
lines.push(`${indent}createEffect(() => {`);
|
|
526
|
+
lines.push(`${indent} const v = __${fieldName}_value();`);
|
|
527
|
+
lines.push(`${indent} if (__${fieldName}_asyncTimer) clearTimeout(__${fieldName}_asyncTimer);`);
|
|
528
|
+
lines.push(`${indent} const version = ++__${fieldName}_asyncVersion;`);
|
|
529
|
+
lines.push(`${indent} __${fieldName}_asyncTimer = setTimeout(async () => {`);
|
|
530
|
+
lines.push(`${indent} try {`);
|
|
531
|
+
lines.push(`${indent} const err = await (${fn})(v);`);
|
|
532
|
+
lines.push(`${indent} if (version === __${fieldName}_asyncVersion) {`);
|
|
533
|
+
lines.push(`${indent} __set_${fieldName}_error(err || null);`);
|
|
534
|
+
lines.push(`${indent} }`);
|
|
535
|
+
lines.push(`${indent} } catch(e) {`);
|
|
536
|
+
lines.push(`${indent} if (version === __${fieldName}_asyncVersion) {`);
|
|
537
|
+
lines.push(`${indent} __set_${fieldName}_error(e.message || "Validation error");`);
|
|
538
|
+
lines.push(`${indent} }`);
|
|
539
|
+
lines.push(`${indent} }`);
|
|
540
|
+
lines.push(`${indent} }, 300);`);
|
|
541
|
+
lines.push(`${indent}});`);
|
|
542
|
+
return lines.join('\n');
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Capitalize the first letter of a string.
|
|
547
|
+
* @param {string} str
|
|
548
|
+
* @returns {string}
|
|
549
|
+
*/
|
|
550
|
+
function capitalize(str) {
|
|
551
|
+
if (!str) return str;
|
|
552
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
553
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Security code generator for the Tova language
|
|
2
|
-
// Produces code fragments consumed by server-codegen and
|
|
2
|
+
// Produces code fragments consumed by server-codegen and browser-codegen.
|
|
3
3
|
|
|
4
4
|
import { BaseCodegen } from './base-codegen.js';
|
|
5
5
|
|
|
@@ -335,9 +335,9 @@ export class SecurityCodegen extends BaseCodegen {
|
|
|
335
335
|
}
|
|
336
336
|
|
|
337
337
|
/**
|
|
338
|
-
* Generate
|
|
338
|
+
* Generate browser-side security code fragments.
|
|
339
339
|
*/
|
|
340
|
-
|
|
340
|
+
generateBrowserSecurity(securityConfig) {
|
|
341
341
|
const lines = [];
|
|
342
342
|
|
|
343
343
|
// Auth token injection for RPC proxy
|
|
@@ -384,7 +384,7 @@ export class SecurityCodegen extends BaseCodegen {
|
|
|
384
384
|
if (securityConfig.roles.length > 0) {
|
|
385
385
|
lines.push('// ── Security: Roles ──');
|
|
386
386
|
lines.push('// NOTE: Client-side role checking is for UI purposes only. All authorization is enforced server-side.');
|
|
387
|
-
lines.push('const
|
|
387
|
+
lines.push('const __browserRoles = {');
|
|
388
388
|
for (const role of securityConfig.roles) {
|
|
389
389
|
const perms = role.permissions.map(p => JSON.stringify(p)).join(', ');
|
|
390
390
|
lines.push(` ${JSON.stringify(role.name)}: [${perms}],`);
|
|
@@ -395,7 +395,7 @@ export class SecurityCodegen extends BaseCodegen {
|
|
|
395
395
|
lines.push('function getUserRole() { return __currentUserRoles; }');
|
|
396
396
|
lines.push('function can(permission) {');
|
|
397
397
|
lines.push(' for (const r of __currentUserRoles) {');
|
|
398
|
-
lines.push(' const perms =
|
|
398
|
+
lines.push(' const perms = __browserRoles[r];');
|
|
399
399
|
lines.push(' if (perms && perms.includes(permission)) return true;');
|
|
400
400
|
lines.push(' }');
|
|
401
401
|
lines.push(' return false;');
|