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