@foormjs/atscript 0.2.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 foormjs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,316 @@
1
+ # @foormjs/atscript
2
+
3
+ ATScript plugin for foormjs -- define forms declaratively with type-safe annotations.
4
+
5
+ Writing form definitions in plain TypeScript means juggling objects, validator functions, and computed property boilerplate. ATScript lets you write all of that as annotations directly on your interface fields. The result is a `.as` file that reads like a specification: labels, placeholders, validators, computed properties, and options are all visible at a glance, right next to the fields they describe.
6
+
7
+ The plugin provides primitives (`foorm.select`, `foorm.radio`, `foorm.checkbox`, `foorm.action`, `foorm.paragraph`), a full set of annotations for field metadata and computed behavior, and `createFoorm()` to convert annotated types into runtime `TFoormModel` objects.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install @foormjs/atscript
13
+ # or
14
+ pnpm add @foormjs/atscript
15
+ ```
16
+
17
+ Peer dependency: `@atscript/typescript` (for type generation).
18
+
19
+ ## Quick Start
20
+
21
+ ### 1. Configure the plugin
22
+
23
+ ```ts
24
+ // atscript.config.ts
25
+ import { defineConfig } from '@atscript/core'
26
+ import ts from '@atscript/typescript'
27
+ import { foormPlugin } from '@foormjs/atscript/plugin'
28
+
29
+ export default defineConfig({
30
+ rootDir: 'src',
31
+ plugins: [ts(), foormPlugin()],
32
+ })
33
+ ```
34
+
35
+ ### 2. Define a form
36
+
37
+ ```
38
+ // login-form.as
39
+ @foorm.title 'Sign In'
40
+ @foorm.submit.text 'Log In'
41
+ export interface LoginForm {
42
+ @meta.label 'Email'
43
+ @meta.placeholder 'you@example.com'
44
+ @foorm.autocomplete 'email'
45
+ @foorm.validate '(v) => !!v || "Email is required"'
46
+ @foorm.validate '(v) => v.includes("@") || "Invalid email"'
47
+ email: string
48
+
49
+ @meta.label 'Password'
50
+ @foorm.type 'password'
51
+ @foorm.validate '(v) => !!v || "Password is required"'
52
+ password: string
53
+
54
+ @meta.label 'Remember me'
55
+ rememberMe?: foorm.checkbox
56
+ }
57
+ ```
58
+
59
+ ### 3. Use at runtime
60
+
61
+ ```ts
62
+ import { createFoorm } from '@foormjs/atscript'
63
+ import { createFormData, getFormValidator } from 'foorm'
64
+ import { LoginForm } from './login-form.as'
65
+
66
+ const form = createFoorm(LoginForm)
67
+ const data = createFormData(form.fields)
68
+ const validator = getFormValidator(form)
69
+ ```
70
+
71
+ ## Practical Examples
72
+
73
+ ### Static form with validation
74
+
75
+ ```
76
+ @foorm.title 'Contact Us'
77
+ @foorm.submit.text 'Send Message'
78
+ export interface ContactForm {
79
+ @meta.label 'Your Name'
80
+ @meta.placeholder 'John Doe'
81
+ @foorm.validate '(v) => !!v || "Name is required"'
82
+ @foorm.order 1
83
+ name: string
84
+
85
+ @meta.label 'Email'
86
+ @foorm.autocomplete 'email'
87
+ @foorm.validate '(v) => !!v || "Email is required"'
88
+ @foorm.validate '(v) => v.includes("@") || "Enter a valid email"'
89
+ @foorm.order 2
90
+ email: string
91
+
92
+ @meta.label 'Message'
93
+ @foorm.validate '(v) => !!v || "Message is required"'
94
+ @foorm.validate '(v) => v.length >= 10 || "At least 10 characters"'
95
+ @foorm.order 3
96
+ message: string
97
+ }
98
+ ```
99
+
100
+ ### Reactive form with computed properties
101
+
102
+ Computed annotations (`@foorm.fn.*`) accept JavaScript function strings. Field-level functions receive `(value, data, context, entry)`, form-level functions receive `(data, context)`.
103
+
104
+ ```
105
+ @foorm.fn.title '(data) => "Welcome, " + (data.firstName || "Guest")'
106
+ @foorm.submit.text 'Register'
107
+ @foorm.fn.submit.disabled '(data) => !data.firstName || !data.lastName'
108
+ export interface SignupForm {
109
+ @meta.label 'First Name'
110
+ @meta.placeholder 'Alice'
111
+ @foorm.validate '(v) => !!v || "Required"'
112
+ @foorm.order 1
113
+ firstName: string
114
+
115
+ @meta.label 'Last Name'
116
+ @foorm.fn.placeholder '(v, data) => data.firstName ? "Same as " + data.firstName + "?" : "Doe"'
117
+ @foorm.validate '(v) => !!v || "Required"'
118
+ @foorm.order 2
119
+ lastName: string
120
+
121
+ @meta.label 'Password'
122
+ @foorm.type 'password'
123
+ @foorm.fn.disabled '(v, data) => !data.firstName || !data.lastName'
124
+ @foorm.validate '(v) => !!v || "Required"'
125
+ @foorm.validate '(v) => v.length >= 8 || "At least 8 characters"'
126
+ @foorm.order 3
127
+ password: string
128
+ }
129
+ ```
130
+
131
+ ### Select, radio, and checkbox fields
132
+
133
+ Use `foorm.select`, `foorm.radio`, and `foorm.checkbox` primitives with `@foorm.options` for static choices or `@foorm.fn.options` for dynamic choices from context:
134
+
135
+ ```
136
+ export interface PreferencesForm {
137
+ // Static options
138
+ @meta.label 'Country'
139
+ @meta.placeholder 'Select a country'
140
+ @foorm.options 'United States', 'us'
141
+ @foorm.options 'Canada', 'ca'
142
+ @foorm.options 'United Kingdom', 'uk'
143
+ country?: foorm.select
144
+
145
+ // Options from context (backend-provided)
146
+ @meta.label 'City'
147
+ @meta.placeholder 'Select a city'
148
+ @foorm.fn.options '(v, data, context) => context.cityOptions || []'
149
+ city?: foorm.select
150
+
151
+ // Radio group
152
+ @meta.label 'Theme'
153
+ @foorm.options 'Light', 'light'
154
+ @foorm.options 'Dark', 'dark'
155
+ @foorm.options 'System', 'system'
156
+ theme?: foorm.radio
157
+
158
+ // Boolean checkbox
159
+ @meta.label 'I agree to the terms and conditions'
160
+ @foorm.validate '(v) => v === true || "You must agree"'
161
+ agreeToTerms: foorm.checkbox
162
+ }
163
+ ```
164
+
165
+ When using `@foorm.fn.options`, the backend passes option lists through the context object:
166
+
167
+ ```ts
168
+ const context = {
169
+ cityOptions: [
170
+ { key: 'nyc', label: 'New York' },
171
+ { key: 'la', label: 'Los Angeles' },
172
+ ],
173
+ }
174
+ ```
175
+
176
+ ### Non-data fields: paragraphs and actions
177
+
178
+ ```
179
+ export interface WizardStep {
180
+ @meta.label 'Please review your information before submitting.'
181
+ info: foorm.paragraph
182
+
183
+ @meta.label 'First Name'
184
+ firstName: string
185
+
186
+ @meta.label 'Reset Form'
187
+ @foorm.altAction 'reset'
188
+ resetBtn: foorm.action
189
+ }
190
+ ```
191
+
192
+ Paragraphs render as static text. Actions render as buttons that emit events instead of submitting the form.
193
+
194
+ ## Annotations Reference
195
+
196
+ ### Form-Level Annotations
197
+
198
+ | Annotation | Description |
199
+ | ------------------------------------------------ | ----------------------------------------------- |
200
+ | `@foorm.title 'text'` | Static form title |
201
+ | `@foorm.submit.text 'text'` | Static submit button text (default: `"Submit"`) |
202
+ | `@foorm.fn.title '(data, ctx) => ...'` | Computed form title |
203
+ | `@foorm.fn.submit.text '(data, ctx) => ...'` | Computed submit button text |
204
+ | `@foorm.fn.submit.disabled '(data, ctx) => ...'` | Computed submit disabled state |
205
+
206
+ ### Field-Level Static Annotations
207
+
208
+ | Annotation | Description |
209
+ | ----------------------------- | ----------------------------------------------------- |
210
+ | `@foorm.type 'text'` | Field input type (text, password, number, date, etc.) |
211
+ | `@foorm.component 'name'` | Named component override for rendering |
212
+ | `@foorm.autocomplete 'value'` | HTML autocomplete attribute |
213
+ | `@foorm.altAction 'name'` | Alternate action name (for action fields) |
214
+ | `@foorm.value 'default'` | Default field value |
215
+ | `@foorm.order N` | Rendering order (lower = earlier) |
216
+ | `@foorm.hidden` | Mark field as statically hidden |
217
+ | `@foorm.disabled` | Mark field as statically disabled |
218
+
219
+ ### Options Annotation
220
+
221
+ | Annotation | Description |
222
+ | --------------------------------- | -------------------------------------------------------------------------------- |
223
+ | `@foorm.options 'Label', 'value'` | Add a static option. Repeat for each choice. Value defaults to label if omitted. |
224
+
225
+ ### Validation Annotation
226
+
227
+ | Annotation | Description |
228
+ | ----------------------------------------- | --------------------------------------------------------------------------------------------- |
229
+ | `@foorm.validate '(v, data, ctx) => ...'` | Custom validator. Returns `true` to pass, or an error string. Repeat for multiple validators. |
230
+
231
+ Validators run in declaration order and stop on first failure.
232
+
233
+ ### Computed (fn) Annotations
234
+
235
+ All field-level computed functions receive `(value, data, context, entry)`:
236
+
237
+ | Annotation | Return type | Description |
238
+ | ----------------------- | ---------------------- | ------------------------------- |
239
+ | `@foorm.fn.label` | `string` | Computed label |
240
+ | `@foorm.fn.description` | `string` | Computed description |
241
+ | `@foorm.fn.hint` | `string` | Computed hint text |
242
+ | `@foorm.fn.placeholder` | `string` | Computed placeholder |
243
+ | `@foorm.fn.disabled` | `boolean` | Computed disabled state |
244
+ | `@foorm.fn.hidden` | `boolean` | Computed hidden state |
245
+ | `@foorm.fn.optional` | `boolean` | Computed optional state |
246
+ | `@foorm.fn.classes` | `string \| Record` | Computed CSS classes |
247
+ | `@foorm.fn.styles` | `string \| Record` | Computed inline styles |
248
+ | `@foorm.fn.options` | `TFoormEntryOptions[]` | Computed options (select/radio) |
249
+
250
+ ### Metadata Annotations (from ATScript core)
251
+
252
+ | Annotation | Description |
253
+ | -------------------------- | ----------------- |
254
+ | `@meta.label 'text'` | Field label |
255
+ | `@meta.description 'text'` | Field description |
256
+ | `@meta.hint 'text'` | Hint text |
257
+ | `@meta.placeholder 'text'` | Input placeholder |
258
+
259
+ ### Constraint Annotations (from ATScript core)
260
+
261
+ | Annotation | Description |
262
+ | --------------------- | --------------------- |
263
+ | `@expect.maxLength N` | Maximum string length |
264
+ | `@expect.minLength N` | Minimum string length |
265
+ | `@expect.min N` | Minimum numeric value |
266
+ | `@expect.max N` | Maximum numeric value |
267
+
268
+ ## Primitives
269
+
270
+ | Primitive | Underlying type | Description |
271
+ | ----------------- | --------------- | --------------------------------- |
272
+ | `foorm.select` | `string` | Dropdown select field |
273
+ | `foorm.radio` | `string` | Radio button group |
274
+ | `foorm.checkbox` | `boolean` | Single checkbox toggle |
275
+ | `foorm.action` | phantom | Button that emits an action event |
276
+ | `foorm.paragraph` | phantom | Static read-only text |
277
+
278
+ Phantom primitives are excluded from form data -- they exist only for UI rendering.
279
+
280
+ ## Plugin Options
281
+
282
+ ```ts
283
+ foormPlugin({
284
+ // Extend allowed values for @foorm.type
285
+ extraTypes: ['tel', 'url', 'color'],
286
+
287
+ // Enable autocomplete for @foorm.component
288
+ components: ['CustomInput', 'DatePicker', 'RichTextEditor'],
289
+ })
290
+ ```
291
+
292
+ When `components` is provided, `@foorm.component` gets IDE autocomplete suggestions and compile-time validation against the list. When omitted, any string is accepted.
293
+
294
+ When `extraTypes` is provided, the additional values are added to the built-in `@foorm.type` values (text, password, number, select, textarea, checkbox, radio, date, paragraph, action).
295
+
296
+ ## Field Type Resolution
297
+
298
+ `createFoorm()` determines each field's type in this order:
299
+
300
+ 1. `@foorm.type` annotation (explicit override)
301
+ 2. Foorm primitive tag (`foorm.select`, `foorm.radio`, `foorm.checkbox`, `foorm.action`, `foorm.paragraph`)
302
+ 3. Default: `'text'`
303
+
304
+ ## Options Resolution
305
+
306
+ For select and radio fields, options are resolved in this order:
307
+
308
+ 1. `@foorm.fn.options` (computed) -- if present, compiled as a function
309
+ 2. `@foorm.options` (static) -- parsed from annotation values
310
+ 3. `undefined` -- no options
311
+
312
+ The computed path takes precedence, so you can define static fallbacks that are overridden when a `fn.options` annotation exists.
313
+
314
+ ## License
315
+
316
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,233 @@
1
+ 'use strict';
2
+
3
+ var deserializeFn = require('@prostojs/deserialize-fn');
4
+
5
+ const pool = new deserializeFn.FNPool();
6
+ /**
7
+ * Compiles a field-level function string from a @foorm.fn.* annotation
8
+ * into a callable function. Uses FNPool for caching.
9
+ *
10
+ * The function string should be an arrow or regular function expression:
11
+ * "(v, data, ctx, entry) => !data.firstName"
12
+ *
13
+ * The compiled function receives a single TFoormFnScope object:
14
+ * { v, data, context, entry }
15
+ */
16
+ function compileFieldFn(fnStr) {
17
+ const code = `return (${fnStr})(v, data, context, entry)`;
18
+ return pool.getFn(code);
19
+ }
20
+ /**
21
+ * Compiles a form-level function string from a @foorm.fn.title,
22
+ * @foorm.fn.submit.text, or @foorm.fn.submit.disabled annotation.
23
+ *
24
+ * The function string should be:
25
+ * "(data, ctx) => someExpression"
26
+ *
27
+ * The compiled function receives a single TFoormFnScope object:
28
+ * { data, context }
29
+ */
30
+ function compileTopFn(fnStr) {
31
+ const code = `return (${fnStr})(data, context)`;
32
+ return pool.getFn(code);
33
+ }
34
+ /**
35
+ * Compiles a validator function string from a @foorm.validate annotation.
36
+ *
37
+ * The function string should be:
38
+ * "(v, data, ctx) => boolean | string"
39
+ *
40
+ * The compiled function receives a single TFoormFnScope object:
41
+ * { v, data, context }
42
+ */
43
+ function compileValidatorFn(fnStr) {
44
+ const code = `return (${fnStr})(v, data, context)`;
45
+ return pool.getFn(code);
46
+ }
47
+
48
+ function foormValidatorPlugin(foormCtx) {
49
+ return (ctx, def, value) => {
50
+ var _a, _b, _c;
51
+ const validators = (_a = def.metadata) === null || _a === void 0 ? void 0 : _a.get('foorm.validate');
52
+ if (!validators) {
53
+ return undefined;
54
+ }
55
+ const fns = Array.isArray(validators) ? validators : [validators];
56
+ const data = (_b = foormCtx === null || foormCtx === void 0 ? void 0 : foormCtx.data) !== null && _b !== void 0 ? _b : {};
57
+ const context = (_c = foormCtx === null || foormCtx === void 0 ? void 0 : foormCtx.context) !== null && _c !== void 0 ? _c : {};
58
+ for (const fnStr of fns) {
59
+ if (typeof fnStr !== 'string') {
60
+ continue;
61
+ }
62
+ const fn = compileValidatorFn(fnStr);
63
+ const result = fn({ v: value, data, context });
64
+ if (result !== true) {
65
+ ctx.error(typeof result === 'string' ? result : 'Validation failed');
66
+ return false;
67
+ }
68
+ }
69
+ return undefined;
70
+ };
71
+ }
72
+
73
+ /** Known foorm primitive extension tags that map directly to field types. */
74
+ const FOORM_TAGS = new Set(['action', 'paragraph', 'select', 'radio', 'checkbox']);
75
+ /** Converts a static @foorm.options annotation value to TFoormEntryOptions[]. */
76
+ function parseStaticOptions(raw) {
77
+ const items = Array.isArray(raw) ? raw : [raw];
78
+ return items.map(item => {
79
+ // Multi-arg annotations are stored as { label, value? }
80
+ if (typeof item === 'object' && item !== null && 'label' in item) {
81
+ const { label, value } = item;
82
+ return value !== undefined ? { key: value, label } : label;
83
+ }
84
+ // Plain string fallback (single-arg or raw value)
85
+ return String(item);
86
+ });
87
+ }
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) {
94
+ const fnStr = metadata.get(fnKey);
95
+ if (typeof fnStr === 'string') {
96
+ return compileFn(fnStr);
97
+ }
98
+ const staticVal = metadata.get(staticKey);
99
+ if (staticVal !== undefined) {
100
+ return staticVal;
101
+ }
102
+ return defaultValue;
103
+ }
104
+ /**
105
+ * Converts an ATScript annotated type into a TFoormModel.
106
+ *
107
+ * Reads @foorm.*, @meta.*, and @expect.* annotations from the type's
108
+ * metadata to build field definitions with static or computed properties.
109
+ *
110
+ * @example
111
+ * ```ts
112
+ * import { RegistrationForm } from './registration.as'
113
+ * import { createFoorm } from '@foormjs/atscript'
114
+ *
115
+ * const model = createFoorm(RegistrationForm)
116
+ * const data = createFormData(model.fields)
117
+ * const validator = getFormValidator(model)
118
+ * ```
119
+ */
120
+ function createFoorm(type) {
121
+ var _a, _b;
122
+ const metadata = type.metadata;
123
+ const props = type.type.props;
124
+ // 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
+ })();
134
+ const submit = { text: submitText, disabled: submitDisabled };
135
+ // Build fields from props
136
+ const fields = [];
137
+ for (const [name, prop] of props.entries()) {
138
+ const pm = prop.metadata;
139
+ const tags = (_a = prop.type) === null || _a === void 0 ? void 0 : _a.tags;
140
+ // Determine field type from @foorm.type, foorm primitive tags, or default
141
+ const foormType = pm.get('foorm.type');
142
+ const foormTag = tags ? [...tags].find(t => FOORM_TAGS.has(t)) : undefined;
143
+ const fieldType = (_b = foormType !== null && foormType !== void 0 ? foormType : foormTag) !== null && _b !== void 0 ? _b : 'text';
144
+ // Build validators from @foorm.validate
145
+ const validators = [];
146
+ const validateAnnotation = pm.get('foorm.validate');
147
+ if (validateAnnotation) {
148
+ const fns = Array.isArray(validateAnnotation) ? validateAnnotation : [validateAnnotation];
149
+ for (const fnStr of fns) {
150
+ if (typeof fnStr === 'string') {
151
+ validators.push(compileValidatorFn(fnStr));
152
+ }
153
+ }
154
+ }
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: (() => {
168
+ 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);
193
+ }
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,
216
+ // 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
+ };
222
+ fields.push(field);
223
+ }
224
+ // Sort by explicit order, preserving original order for unordered fields
225
+ fields.sort((a, b) => { var _a, _b; return ((_a = a.order) !== null && _a !== void 0 ? _a : Infinity) - ((_b = b.order) !== null && _b !== void 0 ? _b : Infinity); });
226
+ return { title, submit, fields };
227
+ }
228
+
229
+ exports.compileFieldFn = compileFieldFn;
230
+ exports.compileTopFn = compileTopFn;
231
+ exports.compileValidatorFn = compileValidatorFn;
232
+ exports.createFoorm = createFoorm;
233
+ exports.foormValidatorPlugin = foormValidatorPlugin;
@@ -0,0 +1,82 @@
1
+ import { TFoormFnScope, TFoormModel } from 'foorm';
2
+ import { TAtscriptAnnotatedType, TAtscriptTypeObject } from '@atscript/typescript/utils';
3
+
4
+ /**
5
+ * ATScript validator plugin that processes @foorm.validate annotations.
6
+ *
7
+ * Reads the `foorm.validate` annotation value(s) from the metadata,
8
+ * compiles each function string, and executes it with the current value
9
+ * and form data/context from validator options.
10
+ *
11
+ * Usage:
12
+ * import { foormValidatorPlugin } from '@foormjs/atscript'
13
+ *
14
+ * const validator = MyForm.validator({
15
+ * plugins: [foormValidatorPlugin()],
16
+ * })
17
+ *
18
+ * Pass form data and context via validator options:
19
+ * validator.validate(fieldValue, true) // safe mode
20
+ *
21
+ * For whole-form validation, iterate props and validate each field.
22
+ */
23
+ interface TFoormValidatorContext {
24
+ data?: Record<string, unknown>;
25
+ context?: Record<string, unknown>;
26
+ }
27
+ type TValidatorPlugin = (ctx: any, def: any, value: unknown) => boolean | undefined;
28
+ declare function foormValidatorPlugin(foormCtx?: TFoormValidatorContext): TValidatorPlugin;
29
+
30
+ /**
31
+ * Compiles a field-level function string from a @foorm.fn.* annotation
32
+ * into a callable function. Uses FNPool for caching.
33
+ *
34
+ * The function string should be an arrow or regular function expression:
35
+ * "(v, data, ctx, entry) => !data.firstName"
36
+ *
37
+ * The compiled function receives a single TFoormFnScope object:
38
+ * { v, data, context, entry }
39
+ */
40
+ declare function compileFieldFn<R = unknown>(fnStr: string): (scope: TFoormFnScope) => R;
41
+ /**
42
+ * Compiles a form-level function string from a @foorm.fn.title,
43
+ * @foorm.fn.submit.text, or @foorm.fn.submit.disabled annotation.
44
+ *
45
+ * The function string should be:
46
+ * "(data, ctx) => someExpression"
47
+ *
48
+ * The compiled function receives a single TFoormFnScope object:
49
+ * { data, context }
50
+ */
51
+ declare function compileTopFn<R = unknown>(fnStr: string): (scope: TFoormFnScope) => R;
52
+ /**
53
+ * Compiles a validator function string from a @foorm.validate annotation.
54
+ *
55
+ * The function string should be:
56
+ * "(v, data, ctx) => boolean | string"
57
+ *
58
+ * The compiled function receives a single TFoormFnScope object:
59
+ * { v, data, context }
60
+ */
61
+ declare function compileValidatorFn(fnStr: string): (scope: TFoormFnScope) => boolean | string;
62
+
63
+ /**
64
+ * Converts an ATScript annotated type into a TFoormModel.
65
+ *
66
+ * Reads @foorm.*, @meta.*, and @expect.* annotations from the type's
67
+ * metadata to build field definitions with static or computed properties.
68
+ *
69
+ * @example
70
+ * ```ts
71
+ * import { RegistrationForm } from './registration.as'
72
+ * import { createFoorm } from '@foormjs/atscript'
73
+ *
74
+ * const model = createFoorm(RegistrationForm)
75
+ * const data = createFormData(model.fields)
76
+ * const validator = getFormValidator(model)
77
+ * ```
78
+ */
79
+ declare function createFoorm(type: TAtscriptAnnotatedType<TAtscriptTypeObject<any, any>>): TFoormModel;
80
+
81
+ export { compileFieldFn, compileTopFn, compileValidatorFn, createFoorm, foormValidatorPlugin };
82
+ export type { TFoormValidatorContext };