@foormjs/atscript 0.2.0 → 0.2.1

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/dist/index.mjs CHANGED
@@ -33,19 +33,19 @@ function compileTopFn(fnStr) {
33
33
  * Compiles a validator function string from a @foorm.validate annotation.
34
34
  *
35
35
  * The function string should be:
36
- * "(v, data, ctx) => boolean | string"
36
+ * "(v, data, ctx, entry) => boolean | string"
37
37
  *
38
38
  * The compiled function receives a single TFoormFnScope object:
39
- * { v, data, context }
39
+ * { v, data, context, entry }
40
40
  */
41
41
  function compileValidatorFn(fnStr) {
42
- const code = `return (${fnStr})(v, data, context)`;
42
+ const code = `return (${fnStr})(v, data, context, entry)`;
43
43
  return pool.getFn(code);
44
44
  }
45
45
 
46
46
  function foormValidatorPlugin(foormCtx) {
47
47
  return (ctx, def, value) => {
48
- var _a, _b, _c;
48
+ var _a, _b, _c, _d, _e;
49
49
  const validators = (_a = def.metadata) === null || _a === void 0 ? void 0 : _a.get('foorm.validate');
50
50
  if (!validators) {
51
51
  return undefined;
@@ -53,12 +53,31 @@ function foormValidatorPlugin(foormCtx) {
53
53
  const fns = Array.isArray(validators) ? validators : [validators];
54
54
  const data = (_b = foormCtx === null || foormCtx === void 0 ? void 0 : foormCtx.data) !== null && _b !== void 0 ? _b : {};
55
55
  const context = (_c = foormCtx === null || foormCtx === void 0 ? void 0 : foormCtx.context) !== null && _c !== void 0 ? _c : {};
56
+ // Build entry object with field metadata
57
+ const entry = {
58
+ field: def.name,
59
+ type: ((_d = def.metadata) === null || _d === void 0 ? void 0 : _d.get('foorm.type')) || 'text',
60
+ component: (_e = def.metadata) === null || _e === void 0 ? void 0 : _e.get('foorm.component'),
61
+ name: def.name,
62
+ };
63
+ // Base scope for evaluating constraints
64
+ const baseScope = { v: value, data, context, entry: undefined };
65
+ // Evaluate computed constraints
66
+ entry.disabled = evalConstraint(def.metadata, 'foorm.disabled', 'foorm.fn.disabled', baseScope);
67
+ entry.optional = evalConstraint(def.metadata, 'foorm.optional', 'foorm.fn.optional', baseScope);
68
+ entry.hidden = evalConstraint(def.metadata, 'foorm.hidden', 'foorm.fn.hidden', baseScope);
69
+ entry.readonly = evalConstraint(def.metadata, 'foorm.readonly', 'foorm.fn.readonly', baseScope);
70
+ // Full scope with evaluated entry
71
+ const scope = { v: value, data, context, entry };
72
+ // Evaluate options (static or computed)
73
+ entry.options = evalOptions(def.metadata, scope);
74
+ // Run custom validators with full scope
56
75
  for (const fnStr of fns) {
57
76
  if (typeof fnStr !== 'string') {
58
77
  continue;
59
78
  }
60
79
  const fn = compileValidatorFn(fnStr);
61
- const result = fn({ v: value, data, context });
80
+ const result = fn(scope);
62
81
  if (result !== true) {
63
82
  ctx.error(typeof result === 'string' ? result : 'Validation failed');
64
83
  return false;
@@ -67,6 +86,429 @@ function foormValidatorPlugin(foormCtx) {
67
86
  return undefined;
68
87
  };
69
88
  }
89
+ /** Helper to evaluate a boolean constraint (static or computed) */
90
+ function evalConstraint(metadata, staticKey, fnKey, scope) {
91
+ const fnStr = metadata === null || metadata === void 0 ? void 0 : metadata.get(fnKey);
92
+ if (typeof fnStr === 'string') {
93
+ return compileFieldFn(fnStr)(scope);
94
+ }
95
+ const staticVal = metadata === null || metadata === void 0 ? void 0 : metadata.get(staticKey);
96
+ return staticVal !== undefined ? true : undefined;
97
+ }
98
+ /** Helper to evaluate options (static or computed) */
99
+ function evalOptions(metadata, scope) {
100
+ const fnStr = metadata === null || metadata === void 0 ? void 0 : metadata.get('foorm.fn.options');
101
+ if (typeof fnStr === 'string') {
102
+ return compileFieldFn(fnStr)(scope);
103
+ }
104
+ const staticOpts = metadata === null || metadata === void 0 ? void 0 : metadata.get('foorm.options');
105
+ if (staticOpts) {
106
+ const items = Array.isArray(staticOpts) ? staticOpts : [staticOpts];
107
+ return items.map(item => {
108
+ if (typeof item === 'object' && item !== null && 'label' in item) {
109
+ const { label, value } = item;
110
+ return value !== undefined ? { key: value, label } : label;
111
+ }
112
+ return String(item);
113
+ });
114
+ }
115
+ return undefined;
116
+ }
117
+
118
+ //#region packages/typescript/src/traverse.ts
119
+ function forAnnotatedType(def, handlers) {
120
+ switch (def.type.kind) {
121
+ case "": {
122
+ const typed = def;
123
+ if (handlers.phantom && typed.type.designType === "phantom") return handlers.phantom(typed);
124
+ return handlers.final(typed);
125
+ }
126
+ case "object": return handlers.object(def);
127
+ case "array": return handlers.array(def);
128
+ case "union": return handlers.union(def);
129
+ case "intersection": return handlers.intersection(def);
130
+ case "tuple": return handlers.tuple(def);
131
+ default: throw new Error(`Unknown type kind "${def.type.kind}"`);
132
+ }
133
+ }
134
+
135
+ //#endregion
136
+ //#region packages/typescript/src/validator.ts
137
+ function _define_property(obj, key, value) {
138
+ if (key in obj) Object.defineProperty(obj, key, {
139
+ value,
140
+ enumerable: true,
141
+ configurable: true,
142
+ writable: true
143
+ });
144
+ else obj[key] = value;
145
+ return obj;
146
+ }
147
+ const regexCache = new Map();
148
+ var Validator = class {
149
+ isLimitExceeded() {
150
+ if (this.stackErrors.length > 0) return this.stackErrors[this.stackErrors.length - 1].length >= this.opts.errorLimit;
151
+ return this.errors.length >= this.opts.errorLimit;
152
+ }
153
+ push(name) {
154
+ this.stackPath.push(name);
155
+ this.stackErrors.push([]);
156
+ }
157
+ pop(saveErrors) {
158
+ this.stackPath.pop();
159
+ const popped = this.stackErrors.pop();
160
+ if (saveErrors && popped?.length) popped.forEach((error) => {
161
+ this.error(error.message, error.path, error.details);
162
+ });
163
+ return popped;
164
+ }
165
+ clear() {
166
+ this.stackErrors[this.stackErrors.length - 1] = [];
167
+ }
168
+ error(message, path, details) {
169
+ const errors = this.stackErrors[this.stackErrors.length - 1] || this.errors;
170
+ const error = {
171
+ path: path || this.path,
172
+ message
173
+ };
174
+ if (details?.length) error.details = details;
175
+ errors.push(error);
176
+ }
177
+ throw() {
178
+ throw new ValidatorError(this.errors);
179
+ }
180
+ /**
181
+ * Validates a value against the type definition.
182
+ *
183
+ * Acts as a TypeScript type guard — when it returns `true`, the value
184
+ * is narrowed to `DataType`.
185
+ *
186
+ * @param value - The value to validate.
187
+ * @param safe - If `true`, returns `false` on failure instead of throwing.
188
+ * @returns `true` if the value matches the type definition.
189
+ * @throws {ValidatorError} When validation fails and `safe` is not `true`.
190
+ */ validate(value, safe) {
191
+ this.push("");
192
+ this.errors = [];
193
+ this.stackErrors = [];
194
+ const passed = this.validateSafe(this.def, value);
195
+ this.pop(!passed);
196
+ if (!passed) {
197
+ if (safe) return false;
198
+ this.throw();
199
+ }
200
+ return true;
201
+ }
202
+ validateSafe(def, value) {
203
+ if (this.isLimitExceeded()) return false;
204
+ if (!isAnnotatedType(def)) throw new Error("Can not validate not-annotated type");
205
+ if (typeof this.opts.replace === "function") def = this.opts.replace(def, this.path);
206
+ if (def.optional && value === undefined) return true;
207
+ for (const plugin of this.opts.plugins) {
208
+ const result = plugin(this, def, value);
209
+ if (result === false || result === true) return result;
210
+ }
211
+ return this.validateAnnotatedType(def, value);
212
+ }
213
+ get path() {
214
+ return this.stackPath.slice(1).join(".");
215
+ }
216
+ validateAnnotatedType(def, value) {
217
+ return forAnnotatedType(def, {
218
+ final: (d) => this.validatePrimitive(d, value),
219
+ phantom: () => true,
220
+ object: (d) => this.validateObject(d, value),
221
+ array: (d) => this.validateArray(d, value),
222
+ union: (d) => this.validateUnion(d, value),
223
+ intersection: (d) => this.validateIntersection(d, value),
224
+ tuple: (d) => this.validateTuple(d, value)
225
+ });
226
+ }
227
+ validateUnion(def, value) {
228
+ let i = 0;
229
+ const popped = [];
230
+ for (const item of def.type.items) {
231
+ this.push(`[${item.type.kind || item.type.designType}(${i})]`);
232
+ if (this.validateSafe(item, value)) {
233
+ this.pop(false);
234
+ return true;
235
+ }
236
+ const errors = this.pop(false);
237
+ if (errors) popped.push(...errors);
238
+ i++;
239
+ }
240
+ this.clear();
241
+ const expected = def.type.items.map((item, i$1) => `[${item.type.kind || item.type.designType}(${i$1})]`).join(", ");
242
+ this.error(`Value does not match any of the allowed types: ${expected}`, undefined, popped);
243
+ return false;
244
+ }
245
+ validateIntersection(def, value) {
246
+ for (const item of def.type.items) if (!this.validateSafe(item, value)) return false;
247
+ return true;
248
+ }
249
+ validateTuple(def, value) {
250
+ if (!Array.isArray(value) || value.length !== def.type.items.length) {
251
+ this.error(`Expected array of length ${def.type.items.length}`);
252
+ return false;
253
+ }
254
+ let i = 0;
255
+ for (const item of def.type.items) {
256
+ this.push(`[${i}]`);
257
+ if (!this.validateSafe(item, value[i])) {
258
+ this.pop(true);
259
+ return false;
260
+ }
261
+ this.pop(false);
262
+ i++;
263
+ }
264
+ return true;
265
+ }
266
+ validateArray(def, value) {
267
+ if (!Array.isArray(value)) {
268
+ this.error("Expected array");
269
+ return false;
270
+ }
271
+ const minLength = def.metadata.get("expect.minLength");
272
+ if (minLength) {
273
+ const length = typeof minLength === "number" ? minLength : minLength.length;
274
+ if (value.length < length) {
275
+ const message = typeof minLength === "object" && minLength.message ? minLength.message : `Expected minimum length of ${length} items, got ${value.length} items`;
276
+ this.error(message);
277
+ return false;
278
+ }
279
+ }
280
+ const maxLength = def.metadata.get("expect.maxLength");
281
+ if (maxLength) {
282
+ const length = typeof maxLength === "number" ? maxLength : maxLength.length;
283
+ if (value.length > length) {
284
+ const message = typeof maxLength === "object" && maxLength.message ? maxLength.message : `Expected maximum length of ${length} items, got ${value.length} items`;
285
+ this.error(message);
286
+ return false;
287
+ }
288
+ }
289
+ let i = 0;
290
+ let passed = true;
291
+ for (const item of value) {
292
+ this.push(`[${i}]`);
293
+ if (!this.validateSafe(def.type.of, item)) {
294
+ passed = false;
295
+ this.pop(true);
296
+ if (this.isLimitExceeded()) return false;
297
+ } else this.pop(false);
298
+ i++;
299
+ }
300
+ return passed;
301
+ }
302
+ validateObject(def, value) {
303
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
304
+ this.error("Expected object");
305
+ return false;
306
+ }
307
+ let passed = true;
308
+ const valueKeys = new Set(Object.keys(value));
309
+ const typeKeys = new Set();
310
+ const skipList = new Set();
311
+ if (this.opts.skipList) {
312
+ const path = this.stackPath.length > 1 ? `${this.path}.` : "";
313
+ this.opts.skipList.forEach((item) => {
314
+ if (item.startsWith(path)) {
315
+ const key = item.slice(path.length);
316
+ skipList.add(key);
317
+ valueKeys.delete(key);
318
+ }
319
+ });
320
+ }
321
+ let partialFunctionMatched = false;
322
+ if (typeof this.opts.partial === "function") partialFunctionMatched = this.opts.partial(def, this.path);
323
+ for (const [key, item] of def.type.props.entries()) {
324
+ if (skipList.has(key) || isPhantomType(item)) continue;
325
+ typeKeys.add(key);
326
+ if (value[key] === undefined) {
327
+ if (partialFunctionMatched || this.opts.partial === "deep" || this.opts.partial === true && this.stackPath.length <= 1) continue;
328
+ }
329
+ this.push(key);
330
+ if (this.validateSafe(item, value[key])) this.pop(false);
331
+ else {
332
+ passed = false;
333
+ this.pop(true);
334
+ if (this.isLimitExceeded()) return false;
335
+ }
336
+ }
337
+ for (const key of valueKeys)
338
+ /** matched patterns for unknown keys */ if (!typeKeys.has(key)) {
339
+ const matched = [];
340
+ for (const { pattern, def: propDef } of def.type.propsPatterns) if (pattern.test(key)) matched.push({
341
+ pattern,
342
+ def: propDef
343
+ });
344
+ if (matched.length) {
345
+ let keyPassed = false;
346
+ for (const { def: def$1 } of matched) if (this.validateSafe(def$1, value[key])) {
347
+ this.pop(false);
348
+ keyPassed = true;
349
+ break;
350
+ }
351
+ if (!keyPassed) {
352
+ this.push(key);
353
+ this.validateSafe(matched[0].def, value[key]);
354
+ this.pop(true);
355
+ passed = false;
356
+ if (this.isLimitExceeded()) return false;
357
+ }
358
+ } else if (this.opts.unknwonProps !== "ignore") {
359
+ if (this.opts.unknwonProps === "error") {
360
+ this.push(key);
361
+ this.error(`Unexpected property`);
362
+ this.pop(true);
363
+ if (this.isLimitExceeded()) return false;
364
+ passed = false;
365
+ } else if (this.opts.unknwonProps === "strip") delete value[key];
366
+ }
367
+ }
368
+ return passed;
369
+ }
370
+ validatePrimitive(def, value) {
371
+ if (typeof def.type.value !== "undefined") {
372
+ if (value !== def.type.value) {
373
+ this.error(`Expected ${def.type.value}, got ${value}`);
374
+ return false;
375
+ }
376
+ return true;
377
+ }
378
+ const typeOfValue = Array.isArray(value) ? "array" : typeof value;
379
+ switch (def.type.designType) {
380
+ case "never":
381
+ this.error(`This type is impossible, must be an internal problem`);
382
+ return false;
383
+ case "any": return true;
384
+ case "string":
385
+ if (typeOfValue !== def.type.designType) {
386
+ this.error(`Expected ${def.type.designType}, got ${typeOfValue}`);
387
+ return false;
388
+ }
389
+ return this.validateString(def, value);
390
+ case "number":
391
+ if (typeOfValue !== def.type.designType) {
392
+ this.error(`Expected ${def.type.designType}, got ${typeOfValue}`);
393
+ return false;
394
+ }
395
+ return this.validateNumber(def, value);
396
+ case "boolean":
397
+ if (typeOfValue !== def.type.designType) {
398
+ this.error(`Expected ${def.type.designType}, got ${typeOfValue}`);
399
+ return false;
400
+ }
401
+ return true;
402
+ case "undefined":
403
+ if (value !== undefined) {
404
+ this.error(`Expected ${def.type.designType}, got ${typeOfValue}`);
405
+ return false;
406
+ }
407
+ return true;
408
+ case "null":
409
+ if (value !== null) {
410
+ this.error(`Expected ${def.type.designType}, got ${typeOfValue}`);
411
+ return false;
412
+ }
413
+ return true;
414
+ default: throw new Error(`Unknown type "${def.type.designType}"`);
415
+ }
416
+ }
417
+ validateString(def, value) {
418
+ const minLength = def.metadata.get("expect.minLength");
419
+ if (minLength) {
420
+ const length = typeof minLength === "number" ? minLength : minLength.length;
421
+ if (value.length < length) {
422
+ const message = typeof minLength === "object" && minLength.message ? minLength.message : `Expected minimum length of ${length} characters, got ${value.length} characters`;
423
+ this.error(message);
424
+ return false;
425
+ }
426
+ }
427
+ const maxLength = def.metadata.get("expect.maxLength");
428
+ if (maxLength) {
429
+ const length = typeof maxLength === "number" ? maxLength : maxLength.length;
430
+ if (value.length > length) {
431
+ const message = typeof maxLength === "object" && maxLength.message ? maxLength.message : `Expected maximum length of ${length} characters, got ${value.length} characters`;
432
+ this.error(message);
433
+ return false;
434
+ }
435
+ }
436
+ const patterns = def.metadata.get("expect.pattern");
437
+ for (const { pattern, flags, message } of patterns || []) {
438
+ if (!pattern) continue;
439
+ const cacheKey = `${pattern}//${flags || ""}`;
440
+ let regex = regexCache.get(cacheKey);
441
+ if (!regex) {
442
+ regex = new RegExp(pattern, flags);
443
+ regexCache.set(cacheKey, regex);
444
+ }
445
+ if (!regex.test(value)) {
446
+ this.error(message || `Value is expected to match pattern "${pattern}"`);
447
+ return false;
448
+ }
449
+ }
450
+ return true;
451
+ }
452
+ validateNumber(def, value) {
453
+ const int = def.metadata.get("expect.int");
454
+ if (int && value % 1 !== 0) {
455
+ const message = typeof int === "object" && int.message ? int.message : `Expected integer, got ${value}`;
456
+ this.error(message);
457
+ return false;
458
+ }
459
+ const min = def.metadata.get("expect.min");
460
+ if (min) {
461
+ const minValue = typeof min === "number" ? min : min.minValue;
462
+ if (value < minValue) {
463
+ const message = typeof min === "object" && min.message ? min.message : `Expected minimum ${minValue}, got ${value}`;
464
+ this.error(message);
465
+ return false;
466
+ }
467
+ }
468
+ const max = def.metadata.get("expect.max");
469
+ if (max) {
470
+ const maxValue = typeof max === "number" ? max : max.maxValue;
471
+ if (value > maxValue) {
472
+ const message = typeof max === "object" && max.message ? max.message : `Expected maximum ${maxValue}, got ${value}`;
473
+ this.error(message);
474
+ return false;
475
+ }
476
+ }
477
+ return true;
478
+ }
479
+ constructor(def, opts) {
480
+ _define_property(this, "def", void 0);
481
+ _define_property(this, "opts", void 0);
482
+ /** Validation errors collected during the last {@link validate} call. */ _define_property(this, "errors", void 0);
483
+ _define_property(this, "stackErrors", void 0);
484
+ _define_property(this, "stackPath", void 0);
485
+ this.def = def;
486
+ this.errors = [];
487
+ this.stackErrors = [];
488
+ this.stackPath = [];
489
+ this.opts = {
490
+ partial: false,
491
+ unknwonProps: "error",
492
+ errorLimit: 10,
493
+ ...opts,
494
+ plugins: opts?.plugins || []
495
+ };
496
+ }
497
+ };
498
+ var ValidatorError = class extends Error {
499
+ constructor(errors) {
500
+ super(`${errors[0].path ? errors[0].path + ": " : ""}${errors[0].message}`), _define_property(this, "errors", void 0), _define_property(this, "name", void 0), this.errors = errors, this.name = "Validation Error";
501
+ }
502
+ };
503
+
504
+ //#endregion
505
+ //#region packages/typescript/src/annotated-type.ts
506
+ function isAnnotatedType(type) {
507
+ return type && type.__is_atscript_annotated_type;
508
+ }
509
+ function isPhantomType(def) {
510
+ return def.type.kind === "" && def.type.designType === "phantom";
511
+ }
70
512
 
71
513
  /** Known foorm primitive extension tags that map directly to field types. */
72
514
  const FOORM_TAGS = new Set(['action', 'paragraph', 'select', 'radio', 'checkbox']);
@@ -83,22 +525,96 @@ function parseStaticOptions(raw) {
83
525
  return String(item);
84
526
  });
85
527
  }
86
- /**
87
- * Resolves a static annotation or a @foorm.fn.* computed annotation.
88
- * If the fn annotation exists, compiles it. Otherwise falls back to the
89
- * static annotation or the default value.
90
- */
91
- function resolveComputed(staticKey, fnKey, metadata, compileFn, defaultValue) {
528
+ // Implementation
529
+ function resolveProperty(fnKey, staticKey, metadata, options) {
530
+ const { transform, defaultValue, staticAsBoolean = false, compiler = compileFieldFn, } = options !== null && options !== void 0 ? options : {};
531
+ // Check for computed annotation first
92
532
  const fnStr = metadata.get(fnKey);
93
533
  if (typeof fnStr === 'string') {
94
- return compileFn(fnStr);
534
+ return compiler(fnStr);
95
535
  }
96
- const staticVal = metadata.get(staticKey);
97
- if (staticVal !== undefined) {
98
- return staticVal;
536
+ // Check for static annotation
537
+ if (staticKey !== undefined) {
538
+ const staticVal = metadata.get(staticKey);
539
+ if (staticVal !== undefined) {
540
+ if (staticAsBoolean) {
541
+ return true;
542
+ }
543
+ if (transform) {
544
+ return transform(staticVal);
545
+ }
546
+ return staticVal;
547
+ }
99
548
  }
549
+ // Return default or undefined
100
550
  return defaultValue;
101
551
  }
552
+ /**
553
+ * Batch resolves multiple boolean constraints at once to reduce overhead.
554
+ * Returns an object with all constraint values (functions or booleans).
555
+ */
556
+ function resolveConstraints(metadata, propOptional) {
557
+ var _a, _b, _c, _d;
558
+ return {
559
+ optional: (_a = resolveProperty('foorm.fn.optional', undefined, metadata)) !== null && _a !== void 0 ? _a : (propOptional !== null && propOptional !== void 0 ? propOptional : false),
560
+ disabled: (_b = resolveProperty('foorm.fn.disabled', 'foorm.disabled', metadata, {
561
+ staticAsBoolean: true,
562
+ })) !== null && _b !== void 0 ? _b : false,
563
+ hidden: (_c = resolveProperty('foorm.fn.hidden', 'foorm.hidden', metadata, {
564
+ staticAsBoolean: true,
565
+ })) !== null && _c !== void 0 ? _c : false,
566
+ readonly: (_d = resolveProperty('foorm.fn.readonly', 'foorm.readonly', metadata, {
567
+ staticAsBoolean: true,
568
+ })) !== null && _d !== void 0 ? _d : false,
569
+ };
570
+ }
571
+ /**
572
+ * Batch resolves multiple text properties at once.
573
+ * Empty strings are replaced with undefined to enable Vue optimization.
574
+ */
575
+ function resolveTextProperties(metadata, fieldName) {
576
+ return {
577
+ label: resolveProperty('foorm.fn.label', 'meta.label', metadata, {
578
+ defaultValue: fieldName,
579
+ }),
580
+ description: resolveProperty('foorm.fn.description', 'meta.description', metadata),
581
+ hint: resolveProperty('foorm.fn.hint', 'meta.hint', metadata),
582
+ placeholder: resolveProperty('foorm.fn.placeholder', 'meta.placeholder', metadata),
583
+ };
584
+ }
585
+ /**
586
+ * Parses @foorm.attr and @foorm.fn.attr annotations into a Record<string, TComputed<unknown>>.
587
+ * Static attrs are direct key-value pairs, computed attrs are compiled functions.
588
+ */
589
+ function parseAttrs(metadata) {
590
+ const staticAttrs = metadata.get('foorm.attr');
591
+ const fnAttrs = metadata.get('foorm.fn.attr');
592
+ if (!staticAttrs && !fnAttrs) {
593
+ return undefined;
594
+ }
595
+ const result = {};
596
+ // Process static @foorm.attr annotations
597
+ if (staticAttrs) {
598
+ const items = Array.isArray(staticAttrs) ? staticAttrs : [staticAttrs];
599
+ for (const item of items) {
600
+ if (typeof item === 'object' && item !== null && 'name' in item && 'value' in item) {
601
+ const { name, value } = item;
602
+ result[name] = value;
603
+ }
604
+ }
605
+ }
606
+ // Process computed @foorm.fn.attr annotations (override static if same name)
607
+ if (fnAttrs) {
608
+ const items = Array.isArray(fnAttrs) ? fnAttrs : [fnAttrs];
609
+ for (const item of items) {
610
+ if (typeof item === 'object' && item !== null && 'name' in item && 'fn' in item) {
611
+ const { name, fn } = item;
612
+ result[name] = compileFieldFn(fn);
613
+ }
614
+ }
615
+ }
616
+ return Object.keys(result).length > 0 ? result : undefined;
617
+ }
102
618
  /**
103
619
  * Converts an ATScript annotated type into a TFoormModel.
104
620
  *
@@ -120,15 +636,18 @@ function createFoorm(type) {
120
636
  const metadata = type.metadata;
121
637
  const props = type.type.props;
122
638
  // Form-level metadata
123
- const title = resolveComputed('foorm.title', 'foorm.fn.title', metadata, compileTopFn, '');
124
- const submitText = resolveComputed('foorm.submit.text', 'foorm.fn.submit.text', metadata, compileTopFn, 'Submit');
125
- const submitDisabled = (() => {
126
- const fnStr = metadata.get('foorm.fn.submit.disabled');
127
- if (typeof fnStr === 'string') {
128
- return compileTopFn(fnStr);
129
- }
130
- return false;
131
- })();
639
+ const title = resolveProperty('foorm.fn.title', 'foorm.title', metadata, {
640
+ compiler: compileTopFn,
641
+ defaultValue: '',
642
+ });
643
+ const submitText = resolveProperty('foorm.fn.submit.text', 'foorm.submit.text', metadata, {
644
+ compiler: compileTopFn,
645
+ defaultValue: 'Submit',
646
+ });
647
+ const submitDisabled = resolveProperty('foorm.fn.submit.disabled', 'foorm.submit.disabled', metadata, {
648
+ compiler: compileTopFn,
649
+ defaultValue: false,
650
+ });
132
651
  const submit = { text: submitText, disabled: submitDisabled };
133
652
  // Build fields from props
134
653
  const fields = [];
@@ -150,73 +669,42 @@ function createFoorm(type) {
150
669
  }
151
670
  }
152
671
  }
153
- const field = {
154
- field: name,
155
- type: fieldType,
156
- component: pm.get('foorm.component'),
157
- autocomplete: pm.get('foorm.autocomplete'),
158
- altAction: pm.get('foorm.altAction'),
159
- order: pm.get('foorm.order'),
160
- name: name,
161
- label: resolveComputed('meta.label', 'foorm.fn.label', pm, compileFieldFn, name),
162
- description: resolveComputed('meta.description', 'foorm.fn.description', pm, compileFieldFn, ''),
163
- hint: resolveComputed('meta.hint', 'foorm.fn.hint', pm, compileFieldFn, ''),
164
- placeholder: resolveComputed('meta.placeholder', 'foorm.fn.placeholder', pm, compileFieldFn, ''),
165
- optional: (() => {
672
+ // Add ATScript built-in validation (for @expect.* and semantic primitives like string.email)
673
+ // Check if field has ATScript validation requirements
674
+ const hasExpectConstraints = pm.get('expect.pattern') !== undefined ||
675
+ pm.get('expect.min') !== undefined ||
676
+ pm.get('expect.max') !== undefined ||
677
+ pm.get('expect.minLength') !== undefined ||
678
+ pm.get('expect.maxLength') !== undefined ||
679
+ pm.get('expect.int') !== undefined;
680
+ const hasTags = tags && tags.size > 0;
681
+ if (hasExpectConstraints || hasTags) {
682
+ // Create ATScript validator for this property
683
+ const propValidator = new Validator(prop);
684
+ validators.push((scope) => {
166
685
  var _a;
167
- const fnStr = pm.get('foorm.fn.optional');
168
- if (typeof fnStr === 'string') {
169
- return compileFieldFn(fnStr);
170
- }
171
- return (_a = prop.optional) !== null && _a !== void 0 ? _a : false;
172
- })(),
173
- disabled: (() => {
174
- const fnStr = pm.get('foorm.fn.disabled');
175
- if (typeof fnStr === 'string') {
176
- return compileFieldFn(fnStr);
177
- }
178
- return pm.get('foorm.disabled') !== undefined;
179
- })(),
180
- hidden: (() => {
181
- const fnStr = pm.get('foorm.fn.hidden');
182
- if (typeof fnStr === 'string') {
183
- return compileFieldFn(fnStr);
184
- }
185
- return pm.get('foorm.hidden') !== undefined;
186
- })(),
187
- classes: (() => {
188
- const fnStr = pm.get('foorm.fn.classes');
189
- if (typeof fnStr === 'string') {
190
- return compileFieldFn(fnStr);
686
+ // ATScript validator in safe mode: returns true (pass) or false (fail)
687
+ // On failure, propValidator.errors contains error details
688
+ const isValid = propValidator.validate(scope.v, true);
689
+ if (isValid) {
690
+ return true;
191
691
  }
192
- return undefined;
193
- })(),
194
- styles: (() => {
195
- const fnStr = pm.get('foorm.fn.styles');
196
- if (typeof fnStr === 'string') {
197
- return compileFieldFn(fnStr);
198
- }
199
- return undefined;
200
- })(),
201
- options: (() => {
202
- const fnStr = pm.get('foorm.fn.options');
203
- if (typeof fnStr === 'string') {
204
- return compileFieldFn(fnStr);
205
- }
206
- const staticOpts = pm.get('foorm.options');
207
- if (staticOpts) {
208
- return parseStaticOptions(staticOpts);
209
- }
210
- return undefined;
211
- })(),
212
- value: pm.get('foorm.value'),
213
- validators,
692
+ // Extract first error message from validator.errors
693
+ const firstError = (_a = propValidator.errors) === null || _a === void 0 ? void 0 : _a[0];
694
+ return (firstError === null || firstError === void 0 ? void 0 : firstError.message) || 'Validation failed';
695
+ });
696
+ }
697
+ const field = Object.assign(Object.assign(Object.assign({ field: name, type: fieldType, component: pm.get('foorm.component'), autocomplete: pm.get('foorm.autocomplete'), altAction: pm.get('foorm.altAction'), order: pm.get('foorm.order'), name: name }, resolveTextProperties(pm, name)), resolveConstraints(pm, prop.optional)), {
698
+ // Appearance - truly optional
699
+ classes: resolveProperty('foorm.fn.classes', undefined, pm), styles: resolveProperty('foorm.fn.styles', undefined, pm),
700
+ // Data properties - truly optional
701
+ options: resolveProperty('foorm.fn.options', 'foorm.options', pm, {
702
+ transform: parseStaticOptions,
703
+ }), value: resolveProperty('foorm.fn.value', 'foorm.value', pm), validators,
704
+ // Custom attributes/props
705
+ attrs: parseAttrs(pm),
214
706
  // ATScript @expect constraints
215
- maxLength: pm.get('expect.maxLength'),
216
- minLength: pm.get('expect.minLength'),
217
- min: pm.get('expect.min'),
218
- max: pm.get('expect.max'),
219
- };
707
+ maxLength: pm.get('expect.maxLength'), minLength: pm.get('expect.minLength'), min: pm.get('expect.min'), max: pm.get('expect.max') });
220
708
  fields.push(field);
221
709
  }
222
710
  // Sort by explicit order, preserving original order for unordered fields