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
@@ -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 client-codegen.
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 client-side security code fragments.
338
+ * Generate browser-side security code fragments.
339
339
  */
340
- generateClientSecurity(securityConfig) {
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 __clientRoles = {');
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 = __clientRoles[r];');
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;');