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
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { BaseCodegen } from './base-codegen.js';
|
|
2
|
-
import {
|
|
2
|
+
import { getBrowserStdlib, buildSelectiveStdlib, RESULT_OPTION, PROPAGATE } from '../stdlib/inline.js';
|
|
3
3
|
import { SecurityCodegen } from './security-codegen.js';
|
|
4
|
+
import { generateValidatorFn, generateFieldSignals, generateFieldAccessor, generateGroupCode, generateArrayCode, generateAsyncValidatorEffect } from './form-codegen.js';
|
|
4
5
|
|
|
5
|
-
export class
|
|
6
|
+
export class BrowserCodegen extends BaseCodegen {
|
|
6
7
|
constructor() {
|
|
7
8
|
super();
|
|
8
9
|
this.stateNames = new Set(); // Track state variable names for setter transforms
|
|
9
10
|
this.computedNames = new Set(); // Track computed variable names for getter transforms
|
|
10
11
|
this.componentNames = new Set(); // Track component names for JSX
|
|
11
12
|
this.storeNames = new Set(); // Track store names
|
|
13
|
+
this.formNames = new Set(); // Track form names
|
|
12
14
|
this._asyncContext = false; // When true, server.xxx() calls emit `await`
|
|
13
15
|
this._rpcCache = new WeakMap(); // Memoize _containsRPC() results
|
|
14
16
|
this._signalCache = new WeakMap(); // Memoize _exprReadsSignal() results
|
|
@@ -185,8 +187,9 @@ export class ClientCodegen extends BaseCodegen {
|
|
|
185
187
|
return `${asyncPrefix}(${params}) => ${this.genExpression(node.body)}`;
|
|
186
188
|
}
|
|
187
189
|
|
|
188
|
-
generate(
|
|
190
|
+
generate(browserBlocks, sharedCode, sharedBuiltins = null, securityConfig = null, typeValidatorsMap = null) {
|
|
189
191
|
this._sharedBuiltins = sharedBuiltins || new Set();
|
|
192
|
+
this._typeValidators = typeValidatorsMap || {};
|
|
190
193
|
const lines = [];
|
|
191
194
|
|
|
192
195
|
// Runtime imports
|
|
@@ -242,7 +245,7 @@ export class ClientCodegen extends BaseCodegen {
|
|
|
242
245
|
// Security block: auth token injection and role helpers
|
|
243
246
|
if (securityConfig) {
|
|
244
247
|
const secGen = new SecurityCodegen();
|
|
245
|
-
const clientSecurity = secGen.
|
|
248
|
+
const clientSecurity = secGen.generateBrowserSecurity(securityConfig);
|
|
246
249
|
if (clientSecurity.trim()) {
|
|
247
250
|
lines.push(clientSecurity);
|
|
248
251
|
lines.push('');
|
|
@@ -254,10 +257,11 @@ export class ClientCodegen extends BaseCodegen {
|
|
|
254
257
|
const effects = [];
|
|
255
258
|
const components = [];
|
|
256
259
|
const stores = [];
|
|
260
|
+
const forms = [];
|
|
257
261
|
const imports = [];
|
|
258
262
|
const other = [];
|
|
259
263
|
|
|
260
|
-
for (const block of
|
|
264
|
+
for (const block of browserBlocks) {
|
|
261
265
|
for (const stmt of block.body) {
|
|
262
266
|
switch (stmt.type) {
|
|
263
267
|
case 'StateDeclaration': states.push(stmt); break;
|
|
@@ -265,6 +269,7 @@ export class ClientCodegen extends BaseCodegen {
|
|
|
265
269
|
case 'EffectDeclaration': effects.push(stmt); break;
|
|
266
270
|
case 'ComponentDeclaration': components.push(stmt); break;
|
|
267
271
|
case 'StoreDeclaration': stores.push(stmt); break;
|
|
272
|
+
case 'FormDeclaration': forms.push(stmt); break;
|
|
268
273
|
case 'ImportDeclaration': imports.push(stmt); break;
|
|
269
274
|
case 'ImportDefault': imports.push(stmt); break;
|
|
270
275
|
case 'ImportWildcard': imports.push(stmt); break;
|
|
@@ -302,6 +307,11 @@ export class ClientCodegen extends BaseCodegen {
|
|
|
302
307
|
this.storeNames.add(store.name);
|
|
303
308
|
}
|
|
304
309
|
|
|
310
|
+
// Register form names
|
|
311
|
+
for (const form of forms) {
|
|
312
|
+
this.formNames.add(form.name);
|
|
313
|
+
}
|
|
314
|
+
|
|
305
315
|
// Generate state signals
|
|
306
316
|
if (states.length > 0) {
|
|
307
317
|
lines.push('// ── Reactive State ──');
|
|
@@ -331,6 +341,15 @@ export class ClientCodegen extends BaseCodegen {
|
|
|
331
341
|
}
|
|
332
342
|
}
|
|
333
343
|
|
|
344
|
+
// Generate forms
|
|
345
|
+
if (forms.length > 0) {
|
|
346
|
+
lines.push('// ── Forms ──');
|
|
347
|
+
for (const form of forms) {
|
|
348
|
+
lines.push(this.generateForm(form));
|
|
349
|
+
lines.push('');
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
334
353
|
// Generate other statements
|
|
335
354
|
for (const stmt of other) {
|
|
336
355
|
lines.push(this.generateStatement(stmt));
|
|
@@ -600,6 +619,12 @@ export class ClientCodegen extends BaseCodegen {
|
|
|
600
619
|
const effectCode = this._generateEffect(node.body);
|
|
601
620
|
this.indent--;
|
|
602
621
|
p.push(`${this.i()}${effectCode}\n`);
|
|
622
|
+
} else if (node.type === 'FormDeclaration') {
|
|
623
|
+
this.formNames.add(node.name);
|
|
624
|
+
p.push(`${this.i()}${this.generateForm(node)}\n`);
|
|
625
|
+
} else if (node.type === 'StoreDeclaration') {
|
|
626
|
+
this.storeNames.add(node.name);
|
|
627
|
+
p.push(`${this.i()}${this.generateStore(node)}\n`);
|
|
603
628
|
} else {
|
|
604
629
|
p.push(this.generateStatement(node) + '\n');
|
|
605
630
|
}
|
|
@@ -697,6 +722,312 @@ export class ClientCodegen extends BaseCodegen {
|
|
|
697
722
|
return p.join('');
|
|
698
723
|
}
|
|
699
724
|
|
|
725
|
+
generateForm(form) {
|
|
726
|
+
// Save/restore state and computed names so form-internal names don't leak
|
|
727
|
+
const savedState = new Set(this.stateNames);
|
|
728
|
+
const savedComputed = new Set(this.computedNames);
|
|
729
|
+
const savedFormNames = new Set(this.formNames);
|
|
730
|
+
|
|
731
|
+
const genExpr = (node) => this.genExpression(node);
|
|
732
|
+
const fields = form.fields || [];
|
|
733
|
+
const groups = form.groups || [];
|
|
734
|
+
const arrays = form.arrays || [];
|
|
735
|
+
const fieldNames = fields.map(f => f.name);
|
|
736
|
+
|
|
737
|
+
// Build merged validators map: field name -> validators array
|
|
738
|
+
// If form has a type annotation, inherit validators from the type definition.
|
|
739
|
+
// Form-level validators override type-level for the same validator name;
|
|
740
|
+
// additional type validators (with different names) are appended.
|
|
741
|
+
const mergedValidatorsMap = {};
|
|
742
|
+
for (const field of fields) {
|
|
743
|
+
mergedValidatorsMap[field.name] = [...(field.validators || [])];
|
|
744
|
+
}
|
|
745
|
+
if (form.typeAnnotation && this._typeValidators) {
|
|
746
|
+
const typeName = form.typeAnnotation.name;
|
|
747
|
+
const typeInfo = this._typeValidators[typeName];
|
|
748
|
+
if (typeInfo) {
|
|
749
|
+
for (const typeField of typeInfo.fields) {
|
|
750
|
+
if (mergedValidatorsMap[typeField.name] !== undefined) {
|
|
751
|
+
const existingNames = new Set(mergedValidatorsMap[typeField.name].map(v => v.name));
|
|
752
|
+
for (const tv of typeField.validators) {
|
|
753
|
+
if (!existingNames.has(tv.name)) {
|
|
754
|
+
mergedValidatorsMap[typeField.name].push(tv);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const p = [];
|
|
763
|
+
p.push(`const ${form.name} = (() => {\n`);
|
|
764
|
+
this.indent++;
|
|
765
|
+
|
|
766
|
+
// Field signals (top-level fields)
|
|
767
|
+
for (const field of fields) {
|
|
768
|
+
const init = field.initialValue ? this.genExpression(field.initialValue) : 'null';
|
|
769
|
+
p.push(generateFieldSignals(field.name, init, this.i()));
|
|
770
|
+
}
|
|
771
|
+
if (fields.length > 0) p.push('\n');
|
|
772
|
+
|
|
773
|
+
// Validator functions (top-level fields) — use merged validators
|
|
774
|
+
for (const field of fields) {
|
|
775
|
+
p.push(generateValidatorFn(field.name, mergedValidatorsMap[field.name] || [], genExpr, this.i()));
|
|
776
|
+
}
|
|
777
|
+
if (fields.length > 0) p.push('\n');
|
|
778
|
+
|
|
779
|
+
// Field accessors (top-level fields)
|
|
780
|
+
for (const field of fields) {
|
|
781
|
+
p.push(generateFieldAccessor(field.name, this.i()));
|
|
782
|
+
}
|
|
783
|
+
if (fields.length > 0) p.push('\n');
|
|
784
|
+
|
|
785
|
+
// Generate groups (signals, validators, accessors, group accessors)
|
|
786
|
+
const allGroupPrefixedNames = []; // Collects ALL prefixed field names from groups
|
|
787
|
+
const conditionalGroups = []; // Tracks conditional groups for form-level isValid
|
|
788
|
+
const groupNames = [];
|
|
789
|
+
const formFieldNameSet = new Set(fieldNames); // For condition expression resolution
|
|
790
|
+
for (const group of groups) {
|
|
791
|
+
p.push(generateGroupCode(group, '', genExpr, this.i(), allGroupPrefixedNames, null, conditionalGroups, formFieldNameSet));
|
|
792
|
+
p.push('\n');
|
|
793
|
+
groupNames.push(group.name);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// Generate arrays (signal-backed item lists)
|
|
797
|
+
const arrayNames = [];
|
|
798
|
+
for (const arr of arrays) {
|
|
799
|
+
p.push(generateArrayCode(arr, genExpr, this.i()));
|
|
800
|
+
p.push('\n');
|
|
801
|
+
arrayNames.push({ name: arr.name, fields: (arr.fields || []).map(f => f.name) });
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// Cross-field re-validation effects (e.g., matches() validator) — use merged validators
|
|
805
|
+
for (const field of fields) {
|
|
806
|
+
const validators = mergedValidatorsMap[field.name] || [];
|
|
807
|
+
for (const v of validators) {
|
|
808
|
+
if (v.name === 'matches') {
|
|
809
|
+
const sourceField = v.args[0] && (v.args[0].name || v.args[0]);
|
|
810
|
+
const depField = field.name;
|
|
811
|
+
if (sourceField) {
|
|
812
|
+
p.push(`${this.i()}createEffect(() => {\n`);
|
|
813
|
+
p.push(`${this.i()} __${sourceField}_value();\n`);
|
|
814
|
+
p.push(`${this.i()} if (__${depField}_touched()) {\n`);
|
|
815
|
+
p.push(`${this.i()} const e = __validate_${depField}(__${depField}_value());\n`);
|
|
816
|
+
p.push(`${this.i()} __set_${depField}_error(e);\n`);
|
|
817
|
+
p.push(`${this.i()} }\n`);
|
|
818
|
+
p.push(`${this.i()}});\n`);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// Async validator effects (debounced, versioned) — use merged validators
|
|
825
|
+
for (const field of fields) {
|
|
826
|
+
const validators = mergedValidatorsMap[field.name] || [];
|
|
827
|
+
for (const v of validators) {
|
|
828
|
+
if (v.isAsync && v.name === 'validate') {
|
|
829
|
+
p.push(generateAsyncValidatorEffect(
|
|
830
|
+
field.name, v, (expr) => this.genExpression(expr), this.i()
|
|
831
|
+
));
|
|
832
|
+
p.push('\n');
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// Form-level signals — include both top-level fields and group-prefixed fields
|
|
838
|
+
const allFieldNamesForValid = [...fieldNames, ...allGroupPrefixedNames];
|
|
839
|
+
const isValidParts = allFieldNamesForValid.map(n => `__${n}_error() === null`);
|
|
840
|
+
// Include array items in isValid: every item in every array must be valid
|
|
841
|
+
const arrayIsValidParts = arrayNames.map(a => `__${a.name}().every(i => i.isValid)`);
|
|
842
|
+
const allIsValidParts = [...isValidParts, ...arrayIsValidParts];
|
|
843
|
+
const isValidExpr = allIsValidParts.length > 0 ? allIsValidParts.join(' && ') : 'true';
|
|
844
|
+
p.push(`${this.i()}const isValid = createComputed(() => ${isValidExpr});\n`);
|
|
845
|
+
|
|
846
|
+
const isDirtyParts = allFieldNamesForValid.map(n => `__${n}_value() !== __${n}_initial`);
|
|
847
|
+
// Include arrays in isDirty: any items added = dirty
|
|
848
|
+
const arrayIsDirtyParts = arrayNames.map(a => `__${a.name}().length > 0`);
|
|
849
|
+
const allIsDirtyParts = [...isDirtyParts, ...arrayIsDirtyParts];
|
|
850
|
+
const isDirtyExpr = allIsDirtyParts.length > 0 ? allIsDirtyParts.join(' || ') : 'false';
|
|
851
|
+
p.push(`${this.i()}const isDirty = createComputed(() => ${isDirtyExpr});\n`);
|
|
852
|
+
|
|
853
|
+
p.push(`${this.i()}const [__submitting, __set_submitting] = createSignal(false);\n`);
|
|
854
|
+
p.push(`${this.i()}const [__submitError, __set_submitError] = createSignal(null);\n`);
|
|
855
|
+
p.push(`${this.i()}const [__submitCount, __set_submitCount] = createSignal(0);\n`);
|
|
856
|
+
p.push('\n');
|
|
857
|
+
|
|
858
|
+
// Wizard steps (only when form has steps block)
|
|
859
|
+
const hasSteps = form.steps && form.steps.steps && form.steps.steps.length > 0;
|
|
860
|
+
if (hasSteps) {
|
|
861
|
+
const fieldNameSet = new Set(fieldNames);
|
|
862
|
+
const groupNameSet = new Set(groupNames);
|
|
863
|
+
const arrayNameSet = new Set(arrayNames.map(a => a.name));
|
|
864
|
+
|
|
865
|
+
p.push(`${this.i()}const [__currentStep, __set_currentStep] = createSignal(0);\n`);
|
|
866
|
+
p.push(`${this.i()}const __steps = [\n`);
|
|
867
|
+
this.indent++;
|
|
868
|
+
for (const step of form.steps.steps) {
|
|
869
|
+
const validateParts = step.members.map(member => {
|
|
870
|
+
if (fieldNameSet.has(member)) return `${member}.validate()`;
|
|
871
|
+
if (groupNameSet.has(member)) return `${member}.isValid`;
|
|
872
|
+
if (arrayNameSet.has(member)) return `${member}.items.every(i => i.isValid)`;
|
|
873
|
+
// Default to field validate
|
|
874
|
+
return `${member}.validate()`;
|
|
875
|
+
});
|
|
876
|
+
const validateExpr = validateParts.length === 1
|
|
877
|
+
? validateParts[0]
|
|
878
|
+
: validateParts.join(' && ');
|
|
879
|
+
p.push(`${this.i()}{ label: ${JSON.stringify(step.label)}, validate: () => ${validateExpr} },\n`);
|
|
880
|
+
}
|
|
881
|
+
this.indent--;
|
|
882
|
+
p.push(`${this.i()}];\n`);
|
|
883
|
+
|
|
884
|
+
p.push(`${this.i()}const canNext = createComputed(() => {\n`);
|
|
885
|
+
this.indent++;
|
|
886
|
+
p.push(`${this.i()}const step = __steps[__currentStep()];\n`);
|
|
887
|
+
p.push(`${this.i()}return step ? step.validate() : false;\n`);
|
|
888
|
+
this.indent--;
|
|
889
|
+
p.push(`${this.i()}});\n`);
|
|
890
|
+
|
|
891
|
+
p.push(`${this.i()}const canPrev = createComputed(() => __currentStep() > 0);\n`);
|
|
892
|
+
p.push(`${this.i()}const progress = createComputed(() => (__currentStep() + 1) / __steps.length);\n`);
|
|
893
|
+
p.push('\n');
|
|
894
|
+
|
|
895
|
+
p.push(`${this.i()}function next() {\n`);
|
|
896
|
+
this.indent++;
|
|
897
|
+
p.push(`${this.i()}if (canNext()) __set_currentStep(__tova_p => __tova_p + 1);\n`);
|
|
898
|
+
this.indent--;
|
|
899
|
+
p.push(`${this.i()}}\n`);
|
|
900
|
+
|
|
901
|
+
p.push(`${this.i()}function prev() {\n`);
|
|
902
|
+
this.indent++;
|
|
903
|
+
p.push(`${this.i()}if (canPrev()) __set_currentStep(__tova_p => __tova_p - 1);\n`);
|
|
904
|
+
this.indent--;
|
|
905
|
+
p.push(`${this.i()}}\n`);
|
|
906
|
+
p.push('\n');
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// Reset function — include both top-level and group-prefixed fields + arrays
|
|
910
|
+
p.push(`${this.i()}function reset() {\n`);
|
|
911
|
+
this.indent++;
|
|
912
|
+
for (const name of fieldNames) {
|
|
913
|
+
p.push(`${this.i()}${name}.reset();\n`);
|
|
914
|
+
}
|
|
915
|
+
for (const name of allGroupPrefixedNames) {
|
|
916
|
+
p.push(`${this.i()}${name}.reset();\n`);
|
|
917
|
+
}
|
|
918
|
+
for (const arr of arrayNames) {
|
|
919
|
+
p.push(`${this.i()}__set_${arr.name}([]); __${arr.name}_nextId = 0;\n`);
|
|
920
|
+
}
|
|
921
|
+
p.push(`${this.i()}__set_submitError(null);\n`);
|
|
922
|
+
this.indent--;
|
|
923
|
+
p.push(`${this.i()}}\n\n`);
|
|
924
|
+
|
|
925
|
+
// Submit function
|
|
926
|
+
p.push(`${this.i()}async function submit(e) {\n`);
|
|
927
|
+
this.indent++;
|
|
928
|
+
p.push(`${this.i()}if (e && e.preventDefault) e.preventDefault();\n`);
|
|
929
|
+
|
|
930
|
+
// Touch all fields to show errors (top-level + group fields)
|
|
931
|
+
for (const name of fieldNames) {
|
|
932
|
+
p.push(`${this.i()}${name}.blur();\n`);
|
|
933
|
+
}
|
|
934
|
+
for (const name of allGroupPrefixedNames) {
|
|
935
|
+
p.push(`${this.i()}${name}.blur();\n`);
|
|
936
|
+
}
|
|
937
|
+
// Touch all array item fields to show errors
|
|
938
|
+
for (const arr of arrayNames) {
|
|
939
|
+
const blurCalls = arr.fields.map(f => `i.${f}.blur()`).join('; ');
|
|
940
|
+
p.push(`${this.i()}__${arr.name}().forEach(i => { ${blurCalls}; });\n`);
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
p.push(`${this.i()}if (!(isValid())) return;\n`);
|
|
944
|
+
p.push(`${this.i()}__set_submitting(true);\n`);
|
|
945
|
+
p.push(`${this.i()}__set_submitError(null);\n`);
|
|
946
|
+
p.push(`${this.i()}__set_submitCount(__tova_p => __tova_p + 1);\n`);
|
|
947
|
+
p.push(`${this.i()}try {\n`);
|
|
948
|
+
this.indent++;
|
|
949
|
+
|
|
950
|
+
// Generate the on submit body
|
|
951
|
+
if (form.onSubmit) {
|
|
952
|
+
p.push(this.genBlockStatements(form.onSubmit));
|
|
953
|
+
p.push('\n');
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
this.indent--;
|
|
957
|
+
p.push(`${this.i()}} catch (err) {\n`);
|
|
958
|
+
this.indent++;
|
|
959
|
+
p.push(`${this.i()}__set_submitError(err.message || String(err));\n`);
|
|
960
|
+
this.indent--;
|
|
961
|
+
p.push(`${this.i()}} finally {\n`);
|
|
962
|
+
this.indent++;
|
|
963
|
+
p.push(`${this.i()}__set_submitting(false);\n`);
|
|
964
|
+
this.indent--;
|
|
965
|
+
p.push(`${this.i()}}\n`);
|
|
966
|
+
this.indent--;
|
|
967
|
+
p.push(`${this.i()}}\n\n`);
|
|
968
|
+
|
|
969
|
+
// Return object
|
|
970
|
+
p.push(`${this.i()}return {\n`);
|
|
971
|
+
this.indent++;
|
|
972
|
+
|
|
973
|
+
// Top-level field accessors
|
|
974
|
+
for (const name of fieldNames) {
|
|
975
|
+
p.push(`${this.i()}${name},\n`);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// Group accessors
|
|
979
|
+
for (const name of groupNames) {
|
|
980
|
+
p.push(`${this.i()}${name},\n`);
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// Array accessors
|
|
984
|
+
for (const arr of arrayNames) {
|
|
985
|
+
p.push(`${this.i()}${arr.name},\n`);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// Values getter — includes top-level fields, group values, and array values
|
|
989
|
+
const topLevelValuesEntries = fieldNames.map(n => `${n}: __${n}_value()`);
|
|
990
|
+
const groupValuesEntries = groupNames.map(n => `${n}: ${n}.values`);
|
|
991
|
+
const arrayValuesEntries = arrayNames.map(a => `${a.name}: __${a.name}().map(i => i.values)`);
|
|
992
|
+
const allValuesEntries = [...topLevelValuesEntries, ...groupValuesEntries, ...arrayValuesEntries];
|
|
993
|
+
const valuesObj = allValuesEntries.join(', ');
|
|
994
|
+
p.push(`${this.i()}get values() { return { ${valuesObj} }; },\n`);
|
|
995
|
+
|
|
996
|
+
// Form-level getters
|
|
997
|
+
p.push(`${this.i()}get isValid() { return isValid(); },\n`);
|
|
998
|
+
p.push(`${this.i()}get isDirty() { return isDirty(); },\n`);
|
|
999
|
+
p.push(`${this.i()}submit,\n`);
|
|
1000
|
+
p.push(`${this.i()}reset,\n`);
|
|
1001
|
+
p.push(`${this.i()}get submitting() { return __submitting(); },\n`);
|
|
1002
|
+
p.push(`${this.i()}get submitError() { return __submitError(); },\n`);
|
|
1003
|
+
p.push(`${this.i()}get submitCount() { return __submitCount(); },\n`);
|
|
1004
|
+
p.push(`${this.i()}setError: (msg) => __set_submitError(msg),\n`);
|
|
1005
|
+
|
|
1006
|
+
// Wizard step properties in return object
|
|
1007
|
+
if (hasSteps) {
|
|
1008
|
+
p.push(`${this.i()}get currentStep() { return __currentStep(); },\n`);
|
|
1009
|
+
p.push(`${this.i()}next,\n`);
|
|
1010
|
+
p.push(`${this.i()}prev,\n`);
|
|
1011
|
+
p.push(`${this.i()}get canNext() { return canNext(); },\n`);
|
|
1012
|
+
p.push(`${this.i()}get canPrev() { return canPrev(); },\n`);
|
|
1013
|
+
p.push(`${this.i()}get progress() { return progress(); },\n`);
|
|
1014
|
+
p.push(`${this.i()}get steps() { return __steps; },\n`);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
this.indent--;
|
|
1018
|
+
p.push(`${this.i()}};\n`);
|
|
1019
|
+
|
|
1020
|
+
this.indent--;
|
|
1021
|
+
p.push(`${this.i()}})();`);
|
|
1022
|
+
|
|
1023
|
+
// Restore state/computed/form names
|
|
1024
|
+
this.stateNames = savedState;
|
|
1025
|
+
this.computedNames = savedComputed;
|
|
1026
|
+
this.formNames = savedFormNames;
|
|
1027
|
+
|
|
1028
|
+
return p.join('');
|
|
1029
|
+
}
|
|
1030
|
+
|
|
700
1031
|
// Check if an AST expression references any signal/computed name (memoized)
|
|
701
1032
|
_exprReadsSignal(node) {
|
|
702
1033
|
if (!node) return false;
|
|
@@ -799,6 +1130,16 @@ export class ClientCodegen extends BaseCodegen {
|
|
|
799
1130
|
return `(__props.children || '')`;
|
|
800
1131
|
}
|
|
801
1132
|
|
|
1133
|
+
// ── <ErrorMessage /> built-in component (compiler-time transform) ──
|
|
1134
|
+
if (node.tag === 'ErrorMessage') {
|
|
1135
|
+
return this._genErrorMessage(node, null);
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
// ── <FormField field={expr}> built-in component (compiler-time transform) ──
|
|
1139
|
+
if (node.tag === 'FormField') {
|
|
1140
|
+
return this._genFormField(node);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
802
1143
|
const isComponent = node.tag[0] === node.tag[0].toUpperCase() && /^[A-Z]/.test(node.tag);
|
|
803
1144
|
|
|
804
1145
|
// Attributes
|
|
@@ -828,6 +1169,10 @@ export class ClientCodegen extends BaseCodegen {
|
|
|
828
1169
|
const valueExpr = isNumeric ? 'Number(e.target.value)' : 'e.target.value';
|
|
829
1170
|
events[eventName] = `(e) => { set${capitalize(exprName)}(${valueExpr}); }`;
|
|
830
1171
|
}
|
|
1172
|
+
} else if (attr.name === 'bind:form') {
|
|
1173
|
+
// Form binding: bind:form={login} → onSubmit wires to form controller's submit()
|
|
1174
|
+
const formName = this.genExpression(attr.value);
|
|
1175
|
+
events.submit = `(e) => ${formName}.submit(e)`;
|
|
831
1176
|
} else if (attr.name === 'bind:checked') {
|
|
832
1177
|
// Two-way binding: bind:checked={flag} → reactive checked + onChange
|
|
833
1178
|
const expr = this.genExpression(attr.value);
|
|
@@ -1119,6 +1464,100 @@ export class ClientCodegen extends BaseCodegen {
|
|
|
1119
1464
|
return result;
|
|
1120
1465
|
}
|
|
1121
1466
|
|
|
1467
|
+
// ── <ErrorMessage /> compiler transform ──
|
|
1468
|
+
// Standalone: <ErrorMessage field={form.email} /> or <ErrorMessage form={login} />
|
|
1469
|
+
// Inside FormField: <ErrorMessage /> (inherits parent field expression)
|
|
1470
|
+
_genErrorMessage(node, parentFieldExpr) {
|
|
1471
|
+
const fieldAttr = node.attributes.find(a => a.name === 'field');
|
|
1472
|
+
const formAttr = node.attributes.find(a => a.name === 'form');
|
|
1473
|
+
|
|
1474
|
+
if (formAttr) {
|
|
1475
|
+
// Form-level error: show form.submitError
|
|
1476
|
+
const formExpr = this.genExpression(formAttr.value);
|
|
1477
|
+
return `() => ${formExpr}.submitError ? tova_el("span", {className: "form-error"}, [() => ${formExpr}.submitError]) : null`;
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
// Determine field expression: explicit attr or inherited from parent FormField
|
|
1481
|
+
let fieldExpr;
|
|
1482
|
+
if (fieldAttr) {
|
|
1483
|
+
fieldExpr = this.genExpression(fieldAttr.value);
|
|
1484
|
+
} else if (parentFieldExpr) {
|
|
1485
|
+
fieldExpr = parentFieldExpr;
|
|
1486
|
+
} else {
|
|
1487
|
+
// No field info — render nothing
|
|
1488
|
+
return 'null';
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
return `() => ${fieldExpr}.touched && ${fieldExpr}.error ? tova_el("span", {className: "form-error"}, [() => ${fieldExpr}.error]) : null`;
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
// ── <FormField field={expr}> compiler transform ──
|
|
1495
|
+
// Generates a wrapper div.form-field with auto-wired input/select/textarea children
|
|
1496
|
+
_genFormField(node) {
|
|
1497
|
+
const fieldAttr = node.attributes.find(a => a.name === 'field');
|
|
1498
|
+
if (!fieldAttr) {
|
|
1499
|
+
// No field attribute — fall through to normal div
|
|
1500
|
+
const children = (node.children || []).map(c => this.genJSX(c)).join(', ');
|
|
1501
|
+
return `tova_el("div", {className: "form-field"}, [${children}])`;
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
const fieldExpr = this.genExpression(fieldAttr.value);
|
|
1505
|
+
const inputTags = new Set(['input', 'select', 'textarea']);
|
|
1506
|
+
|
|
1507
|
+
const childParts = [];
|
|
1508
|
+
for (const child of (node.children || [])) {
|
|
1509
|
+
if (child.type === 'JSXElement' && inputTags.has(child.tag)) {
|
|
1510
|
+
// Auto-wire input/select/textarea to the field
|
|
1511
|
+
childParts.push(this._genFormFieldInput(child, fieldExpr));
|
|
1512
|
+
} else if (child.type === 'JSXElement' && child.tag === 'ErrorMessage') {
|
|
1513
|
+
// Replace <ErrorMessage /> with conditional error display using parent field
|
|
1514
|
+
childParts.push(this._genErrorMessage(child, fieldExpr));
|
|
1515
|
+
} else {
|
|
1516
|
+
// Other children: generate normally
|
|
1517
|
+
childParts.push(this.genJSX(child));
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
return `tova_el("div", {className: "form-field"}, [${childParts.join(', ')}])`;
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
// Generate an input/select/textarea element with field bindings injected
|
|
1525
|
+
_genFormFieldInput(node, fieldExpr) {
|
|
1526
|
+
// Process existing attributes normally, then inject field bindings
|
|
1527
|
+
const attrs = {};
|
|
1528
|
+
const events = {};
|
|
1529
|
+
|
|
1530
|
+
for (const attr of node.attributes) {
|
|
1531
|
+
if (attr.name === 'class') {
|
|
1532
|
+
attrs.className = this.genExpression(attr.value);
|
|
1533
|
+
} else if (attr.name.startsWith('on:')) {
|
|
1534
|
+
events[attr.name.slice(3)] = this.genExpression(attr.value);
|
|
1535
|
+
} else {
|
|
1536
|
+
attrs[attr.name] = this.genExpression(attr.value);
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
// Inject field bindings
|
|
1541
|
+
attrs.value = `() => ${fieldExpr}.value`;
|
|
1542
|
+
events.input = `(e) => ${fieldExpr}.set(e.target.value)`;
|
|
1543
|
+
events.blur = `() => ${fieldExpr}.blur()`;
|
|
1544
|
+
|
|
1545
|
+
const propParts = [];
|
|
1546
|
+
for (const [key, val] of Object.entries(attrs)) {
|
|
1547
|
+
propParts.push(`${key}: ${val}`);
|
|
1548
|
+
}
|
|
1549
|
+
for (const [event, handler] of Object.entries(events)) {
|
|
1550
|
+
propParts.push(`on${event[0].toUpperCase() + event.slice(1)}: ${handler}`);
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
const tag = JSON.stringify(node.tag);
|
|
1554
|
+
if (node.selfClosing || !node.children || node.children.length === 0) {
|
|
1555
|
+
return `tova_el(${tag}, {${propParts.join(', ')}})`;
|
|
1556
|
+
}
|
|
1557
|
+
const children = node.children.map(c => this.genJSX(c)).join(', ');
|
|
1558
|
+
return `tova_el(${tag}, {${propParts.join(', ')}}, [${children}])`;
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1122
1561
|
genJSXText(node) {
|
|
1123
1562
|
if (node.value.type === 'StringLiteral') {
|
|
1124
1563
|
return JSON.stringify(node.value.value);
|