@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/dist/index.mjs ADDED
@@ -0,0 +1,227 @@
1
+ import { FNPool } from '@prostojs/deserialize-fn';
2
+
3
+ const pool = new FNPool();
4
+ /**
5
+ * Compiles a field-level function string from a @foorm.fn.* annotation
6
+ * into a callable function. Uses FNPool for caching.
7
+ *
8
+ * The function string should be an arrow or regular function expression:
9
+ * "(v, data, ctx, entry) => !data.firstName"
10
+ *
11
+ * The compiled function receives a single TFoormFnScope object:
12
+ * { v, data, context, entry }
13
+ */
14
+ function compileFieldFn(fnStr) {
15
+ const code = `return (${fnStr})(v, data, context, entry)`;
16
+ return pool.getFn(code);
17
+ }
18
+ /**
19
+ * Compiles a form-level function string from a @foorm.fn.title,
20
+ * @foorm.fn.submit.text, or @foorm.fn.submit.disabled annotation.
21
+ *
22
+ * The function string should be:
23
+ * "(data, ctx) => someExpression"
24
+ *
25
+ * The compiled function receives a single TFoormFnScope object:
26
+ * { data, context }
27
+ */
28
+ function compileTopFn(fnStr) {
29
+ const code = `return (${fnStr})(data, context)`;
30
+ return pool.getFn(code);
31
+ }
32
+ /**
33
+ * Compiles a validator function string from a @foorm.validate annotation.
34
+ *
35
+ * The function string should be:
36
+ * "(v, data, ctx) => boolean | string"
37
+ *
38
+ * The compiled function receives a single TFoormFnScope object:
39
+ * { v, data, context }
40
+ */
41
+ function compileValidatorFn(fnStr) {
42
+ const code = `return (${fnStr})(v, data, context)`;
43
+ return pool.getFn(code);
44
+ }
45
+
46
+ function foormValidatorPlugin(foormCtx) {
47
+ return (ctx, def, value) => {
48
+ var _a, _b, _c;
49
+ const validators = (_a = def.metadata) === null || _a === void 0 ? void 0 : _a.get('foorm.validate');
50
+ if (!validators) {
51
+ return undefined;
52
+ }
53
+ const fns = Array.isArray(validators) ? validators : [validators];
54
+ const data = (_b = foormCtx === null || foormCtx === void 0 ? void 0 : foormCtx.data) !== null && _b !== void 0 ? _b : {};
55
+ const context = (_c = foormCtx === null || foormCtx === void 0 ? void 0 : foormCtx.context) !== null && _c !== void 0 ? _c : {};
56
+ for (const fnStr of fns) {
57
+ if (typeof fnStr !== 'string') {
58
+ continue;
59
+ }
60
+ const fn = compileValidatorFn(fnStr);
61
+ const result = fn({ v: value, data, context });
62
+ if (result !== true) {
63
+ ctx.error(typeof result === 'string' ? result : 'Validation failed');
64
+ return false;
65
+ }
66
+ }
67
+ return undefined;
68
+ };
69
+ }
70
+
71
+ /** Known foorm primitive extension tags that map directly to field types. */
72
+ const FOORM_TAGS = new Set(['action', 'paragraph', 'select', 'radio', 'checkbox']);
73
+ /** Converts a static @foorm.options annotation value to TFoormEntryOptions[]. */
74
+ function parseStaticOptions(raw) {
75
+ const items = Array.isArray(raw) ? raw : [raw];
76
+ return items.map(item => {
77
+ // Multi-arg annotations are stored as { label, value? }
78
+ if (typeof item === 'object' && item !== null && 'label' in item) {
79
+ const { label, value } = item;
80
+ return value !== undefined ? { key: value, label } : label;
81
+ }
82
+ // Plain string fallback (single-arg or raw value)
83
+ return String(item);
84
+ });
85
+ }
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) {
92
+ const fnStr = metadata.get(fnKey);
93
+ if (typeof fnStr === 'string') {
94
+ return compileFn(fnStr);
95
+ }
96
+ const staticVal = metadata.get(staticKey);
97
+ if (staticVal !== undefined) {
98
+ return staticVal;
99
+ }
100
+ return defaultValue;
101
+ }
102
+ /**
103
+ * Converts an ATScript annotated type into a TFoormModel.
104
+ *
105
+ * Reads @foorm.*, @meta.*, and @expect.* annotations from the type's
106
+ * metadata to build field definitions with static or computed properties.
107
+ *
108
+ * @example
109
+ * ```ts
110
+ * import { RegistrationForm } from './registration.as'
111
+ * import { createFoorm } from '@foormjs/atscript'
112
+ *
113
+ * const model = createFoorm(RegistrationForm)
114
+ * const data = createFormData(model.fields)
115
+ * const validator = getFormValidator(model)
116
+ * ```
117
+ */
118
+ function createFoorm(type) {
119
+ var _a, _b;
120
+ const metadata = type.metadata;
121
+ const props = type.type.props;
122
+ // 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
+ })();
132
+ const submit = { text: submitText, disabled: submitDisabled };
133
+ // Build fields from props
134
+ const fields = [];
135
+ for (const [name, prop] of props.entries()) {
136
+ const pm = prop.metadata;
137
+ const tags = (_a = prop.type) === null || _a === void 0 ? void 0 : _a.tags;
138
+ // Determine field type from @foorm.type, foorm primitive tags, or default
139
+ const foormType = pm.get('foorm.type');
140
+ const foormTag = tags ? [...tags].find(t => FOORM_TAGS.has(t)) : undefined;
141
+ const fieldType = (_b = foormType !== null && foormType !== void 0 ? foormType : foormTag) !== null && _b !== void 0 ? _b : 'text';
142
+ // Build validators from @foorm.validate
143
+ const validators = [];
144
+ const validateAnnotation = pm.get('foorm.validate');
145
+ if (validateAnnotation) {
146
+ const fns = Array.isArray(validateAnnotation) ? validateAnnotation : [validateAnnotation];
147
+ for (const fnStr of fns) {
148
+ if (typeof fnStr === 'string') {
149
+ validators.push(compileValidatorFn(fnStr));
150
+ }
151
+ }
152
+ }
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: (() => {
166
+ 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);
191
+ }
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,
214
+ // 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
+ };
220
+ fields.push(field);
221
+ }
222
+ // Sort by explicit order, preserving original order for unordered fields
223
+ 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); });
224
+ return { title, submit, fields };
225
+ }
226
+
227
+ export { compileFieldFn, compileTopFn, compileValidatorFn, createFoorm, foormValidatorPlugin };
@@ -0,0 +1,303 @@
1
+ 'use strict';
2
+
3
+ var core = require('@atscript/core');
4
+
5
+ /**
6
+ * Attempts to compile a function string with `new Function` and returns
7
+ * diagnostic errors if it fails. Used by @foorm.fn.* and @foorm.validate
8
+ * annotation validate hooks.
9
+ */
10
+ function validateFnString(fnStr, range) {
11
+ try {
12
+ // eslint-disable-next-line no-new-func
13
+ new Function('v', 'data', 'context', 'entry', `return (${fnStr})(v, data, context, entry)`);
14
+ }
15
+ catch (error) {
16
+ return [
17
+ {
18
+ severity: 1,
19
+ message: `Invalid function string: ${error.message}`,
20
+ range,
21
+ },
22
+ ];
23
+ }
24
+ return undefined;
25
+ }
26
+ function fnAnnotation(description) {
27
+ return new core.AnnotationSpec({
28
+ description,
29
+ nodeType: ['prop'],
30
+ argument: {
31
+ name: 'fn',
32
+ type: 'string',
33
+ description: 'JS function string: (value, data, context, entry) => result',
34
+ },
35
+ validate(token, args) {
36
+ if (args[0]) {
37
+ return validateFnString(args[0].text, args[0].range);
38
+ }
39
+ return undefined;
40
+ },
41
+ });
42
+ }
43
+ function fnTopAnnotation(description) {
44
+ return new core.AnnotationSpec({
45
+ description,
46
+ nodeType: ['interface'],
47
+ argument: {
48
+ name: 'fn',
49
+ type: 'string',
50
+ description: 'JS function string: (data, context) => result',
51
+ },
52
+ validate(token, args) {
53
+ if (args[0]) {
54
+ return validateFnString(args[0].text, args[0].range);
55
+ }
56
+ return undefined;
57
+ },
58
+ });
59
+ }
60
+ const annotations = {
61
+ foorm: {
62
+ // ── Form-level static annotations ────────────────────────
63
+ title: new core.AnnotationSpec({
64
+ description: 'Static form title',
65
+ nodeType: ['interface'],
66
+ argument: {
67
+ name: 'title',
68
+ type: 'string',
69
+ description: 'The form title text',
70
+ },
71
+ }),
72
+ submit: {
73
+ text: new core.AnnotationSpec({
74
+ description: 'Static submit button text',
75
+ nodeType: ['interface'],
76
+ argument: {
77
+ name: 'text',
78
+ type: 'string',
79
+ description: 'Submit button label',
80
+ },
81
+ }),
82
+ },
83
+ // ── Field-level static annotations ───────────────────────
84
+ type: new core.AnnotationSpec({
85
+ description: 'Field input type',
86
+ nodeType: ['prop'],
87
+ argument: {
88
+ name: 'type',
89
+ type: 'string',
90
+ values: [
91
+ 'text',
92
+ 'password',
93
+ 'number',
94
+ 'select',
95
+ 'textarea',
96
+ 'checkbox',
97
+ 'radio',
98
+ 'date',
99
+ 'paragraph',
100
+ 'action',
101
+ ],
102
+ description: 'The input type for this field',
103
+ },
104
+ }),
105
+ component: new core.AnnotationSpec({
106
+ description: 'Named component override for rendering this field',
107
+ nodeType: ['prop'],
108
+ argument: {
109
+ name: 'name',
110
+ type: 'string',
111
+ description: 'Component name from the components registry',
112
+ },
113
+ }),
114
+ autocomplete: new core.AnnotationSpec({
115
+ description: 'HTML autocomplete attribute value',
116
+ nodeType: ['prop'],
117
+ argument: {
118
+ name: 'value',
119
+ type: 'string',
120
+ description: 'Autocomplete value (e.g., "email", "given-name")',
121
+ },
122
+ }),
123
+ altAction: new core.AnnotationSpec({
124
+ description: 'Alternate submit action name for this field',
125
+ nodeType: ['prop'],
126
+ argument: {
127
+ name: 'action',
128
+ type: 'string',
129
+ description: 'The action name emitted on submit',
130
+ },
131
+ }),
132
+ value: new core.AnnotationSpec({
133
+ description: 'Default value for this field',
134
+ nodeType: ['prop'],
135
+ argument: {
136
+ name: 'value',
137
+ type: 'string',
138
+ description: 'Default value (parsed by field type at runtime)',
139
+ },
140
+ }),
141
+ order: new core.AnnotationSpec({
142
+ description: 'Explicit rendering order for this field',
143
+ nodeType: ['prop'],
144
+ argument: {
145
+ name: 'order',
146
+ type: 'number',
147
+ description: 'Numeric order (lower = earlier)',
148
+ },
149
+ }),
150
+ hidden: new core.AnnotationSpec({
151
+ description: 'Statically mark this field as hidden',
152
+ nodeType: ['prop'],
153
+ }),
154
+ disabled: new core.AnnotationSpec({
155
+ description: 'Statically mark this field as disabled',
156
+ nodeType: ['prop'],
157
+ }),
158
+ // ── Options annotation ──────────────────────────────────
159
+ options: new core.AnnotationSpec({
160
+ description: 'Static option for select/radio fields. Repeat for each option. Label is the display text, value is the key (defaults to label).',
161
+ nodeType: ['prop'],
162
+ multiple: true,
163
+ mergeStrategy: 'replace',
164
+ argument: [
165
+ {
166
+ name: 'label',
167
+ type: 'string',
168
+ description: 'Display label for the option',
169
+ },
170
+ {
171
+ name: 'value',
172
+ type: 'string',
173
+ optional: true,
174
+ description: 'Value/key for the option (defaults to label if omitted)',
175
+ },
176
+ ],
177
+ }),
178
+ // ── Validation annotation ────────────────────────────────
179
+ validate: new core.AnnotationSpec({
180
+ description: 'Custom JS validator function string. Returns true for pass, or an error message string.',
181
+ nodeType: ['prop'],
182
+ multiple: true,
183
+ mergeStrategy: 'append',
184
+ argument: {
185
+ name: 'fn',
186
+ type: 'string',
187
+ description: 'JS function string: (value, data, context) => boolean | string',
188
+ },
189
+ validate(token, args) {
190
+ if (args[0]) {
191
+ return validateFnString(args[0].text, args[0].range);
192
+ }
193
+ return undefined;
194
+ },
195
+ }),
196
+ // ── Computed (fn) annotations ────────────────────────────
197
+ fn: {
198
+ // Form-level computed
199
+ title: fnTopAnnotation('Computed form title: (data, context) => string'),
200
+ submit: {
201
+ text: fnTopAnnotation('Computed submit button text: (data, context) => string'),
202
+ disabled: fnTopAnnotation('Computed submit disabled state: (data, context) => boolean'),
203
+ },
204
+ // Field-level computed
205
+ label: fnAnnotation('Computed label: (value, data, context, entry) => string'),
206
+ description: fnAnnotation('Computed description: (value, data, context, entry) => string'),
207
+ hint: fnAnnotation('Computed hint: (value, data, context, entry) => string'),
208
+ placeholder: fnAnnotation('Computed placeholder: (value, data, context, entry) => string'),
209
+ disabled: fnAnnotation('Computed disabled state: (value, data, context, entry) => boolean'),
210
+ hidden: fnAnnotation('Computed hidden state: (value, data, context, entry) => boolean'),
211
+ optional: fnAnnotation('Computed optional state: (value, data, context, entry) => boolean'),
212
+ classes: fnAnnotation('Computed CSS classes: (value, data, context, entry) => string | Record<string, boolean>'),
213
+ styles: fnAnnotation('Computed inline styles: (value, data, context, entry) => string | Record<string, string>'),
214
+ options: fnAnnotation('Computed select/radio options: (value, data, context, entry) => Array'),
215
+ },
216
+ },
217
+ };
218
+
219
+ const primitives = {
220
+ foorm: {
221
+ type: 'phantom',
222
+ isContainer: true,
223
+ documentation: 'Non-data UI elements for form rendering',
224
+ extensions: {
225
+ action: {
226
+ documentation: 'Form action button — not a data field, excluded from form data. Use with @foorm.altAction to define alternate submit actions.',
227
+ },
228
+ paragraph: {
229
+ documentation: 'Read-only paragraph text — rendered as static content, not an input field. Use @meta.label for the paragraph text.',
230
+ },
231
+ select: {
232
+ type: 'string',
233
+ documentation: 'Dropdown select field. Use @foorm.options to define static choices or @foorm.fn.options for computed choices.',
234
+ },
235
+ radio: {
236
+ type: 'string',
237
+ documentation: 'Radio button group. Use @foorm.options to define static choices or @foorm.fn.options for computed choices.',
238
+ },
239
+ checkbox: {
240
+ type: 'boolean',
241
+ documentation: 'Single boolean checkbox toggle.',
242
+ },
243
+ },
244
+ },
245
+ };
246
+
247
+ const BUILTIN_TYPES = [
248
+ 'text',
249
+ 'password',
250
+ 'number',
251
+ 'select',
252
+ 'textarea',
253
+ 'checkbox',
254
+ 'radio',
255
+ 'date',
256
+ 'paragraph',
257
+ 'action',
258
+ ];
259
+ function foormPlugin(opts) {
260
+ return {
261
+ name: 'foorm',
262
+ config() {
263
+ var _a, _b, _c, _d;
264
+ if (!((_a = opts === null || opts === void 0 ? void 0 : opts.extraTypes) === null || _a === void 0 ? void 0 : _a.length) && !((_b = opts === null || opts === void 0 ? void 0 : opts.components) === null || _b === void 0 ? void 0 : _b.length)) {
265
+ return { primitives, annotations };
266
+ }
267
+ const foormNs = annotations.foorm;
268
+ const overrides = {};
269
+ if ((_c = opts.extraTypes) === null || _c === void 0 ? void 0 : _c.length) {
270
+ overrides.type = new core.AnnotationSpec({
271
+ description: 'Field input type',
272
+ nodeType: ['prop'],
273
+ argument: {
274
+ name: 'type',
275
+ type: 'string',
276
+ values: [...BUILTIN_TYPES, ...opts.extraTypes],
277
+ description: 'The input type for this field',
278
+ },
279
+ });
280
+ }
281
+ if ((_d = opts.components) === null || _d === void 0 ? void 0 : _d.length) {
282
+ overrides.component = new core.AnnotationSpec({
283
+ description: 'Named component override for rendering this field',
284
+ nodeType: ['prop'],
285
+ argument: {
286
+ name: 'name',
287
+ type: 'string',
288
+ values: opts.components,
289
+ description: 'Component name from the components registry',
290
+ },
291
+ });
292
+ }
293
+ return {
294
+ primitives,
295
+ annotations: Object.assign(Object.assign({}, annotations), { foorm: Object.assign(Object.assign({}, foormNs), overrides) }),
296
+ };
297
+ },
298
+ };
299
+ }
300
+
301
+ exports.annotations = annotations;
302
+ exports.foormPlugin = foormPlugin;
303
+ exports.primitives = primitives;
@@ -0,0 +1,23 @@
1
+ import { TAnnotationsTree, TAtscriptConfig, TAtscriptPlugin } from '@atscript/core';
2
+
3
+ declare const annotations: TAnnotationsTree;
4
+
5
+ declare const primitives: TAtscriptConfig['primitives'];
6
+
7
+ interface TFoormPluginOptions {
8
+ /**
9
+ * Additional field type values to allow in @foorm.type annotation.
10
+ * Built-in values: text, password, number, select, textarea, checkbox, radio, date, paragraph, action
11
+ */
12
+ extraTypes?: string[];
13
+ /**
14
+ * List of custom component names available in the project.
15
+ * Enables IDE autocomplete and validation for @foorm.component annotation.
16
+ * When omitted, @foorm.component accepts any string.
17
+ */
18
+ components?: string[];
19
+ }
20
+ declare function foormPlugin(opts?: TFoormPluginOptions): TAtscriptPlugin;
21
+
22
+ export { annotations, foormPlugin, primitives };
23
+ export type { TFoormPluginOptions };