@formspec/decorators 0.1.0-alpha.3

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/README.md ADDED
@@ -0,0 +1,179 @@
1
+ # @formspec/decorators
2
+
3
+ Decorator stubs for FormSpec CLI static analysis.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @formspec/decorators
9
+ # or
10
+ pnpm add @formspec/decorators
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```typescript
16
+ import { Label, Min, Max, EnumOptions, ShowWhen, Group } from '@formspec/decorators';
17
+
18
+ class UserRegistration {
19
+ @Group("Personal Info")
20
+ @Label("Full Name")
21
+ name!: string;
22
+
23
+ @Group("Personal Info")
24
+ @Label("Age")
25
+ @Min(18)
26
+ @Max(120)
27
+ age?: number;
28
+
29
+ @Group("Preferences")
30
+ @Label("Country")
31
+ @EnumOptions([
32
+ { id: "us", label: "United States" },
33
+ { id: "ca", label: "Canada" },
34
+ { id: "uk", label: "United Kingdom" }
35
+ ])
36
+ country!: "us" | "ca" | "uk";
37
+
38
+ @Group("Preferences")
39
+ @Label("Contact Method")
40
+ contactMethod!: "email" | "phone";
41
+
42
+ @ShowWhen({ field: "contactMethod", value: "email" })
43
+ @Label("Email Address")
44
+ email?: string;
45
+
46
+ @ShowWhen({ field: "contactMethod", value: "phone" })
47
+ @Label("Phone Number")
48
+ phone?: string;
49
+ }
50
+ ```
51
+
52
+ ## Generating Schemas
53
+
54
+ ### Build-Time Only
55
+
56
+ Generate JSON Schema and UI Schema files at build time:
57
+
58
+ ```bash
59
+ formspec generate ./src/user-registration.ts UserRegistration -o ./generated
60
+ ```
61
+
62
+ This outputs static JSON files to `./generated/`. No codegen step required.
63
+
64
+ ### Runtime Schema Generation
65
+
66
+ If you need JSON Schema or UI Schema **at runtime in your program** (e.g., dynamic form rendering, server-side generation), you have two options:
67
+
68
+ 1. **Chain DSL** - Works at runtime without any codegen step. See the [Chain DSL documentation](../dsl/README.md).
69
+
70
+ 2. **Decorator DSL with codegen** - If you prefer to keep using decorated classes, run codegen to preserve type information:
71
+
72
+ ```bash
73
+ # Generate type metadata file
74
+ formspec codegen ./src/forms.ts -o ./src/__formspec_types__.ts
75
+ ```
76
+
77
+ ```typescript
78
+ // Import the generated file at your application entry point
79
+ import './__formspec_types__.js';
80
+
81
+ // Now buildFormSchemas() has access to full type information
82
+ import { buildFormSchemas } from '@formspec/decorators';
83
+ import { UserRegistration } from './forms.js';
84
+
85
+ const { jsonSchema, uiSchema } = buildFormSchemas(UserRegistration);
86
+ // jsonSchema: { $schema: "...", type: "object", properties: {...}, required: [...] }
87
+ // uiSchema: { type: "VerticalLayout", elements: [...] }
88
+ ```
89
+
90
+ Add `formspec codegen` to your build process to keep type metadata in sync.
91
+
92
+ > **Note:** If you need to work with **dynamically fetched schema data** (schemas not known at build time), use the Chain DSL. It's the only option for dynamic schemas.
93
+
94
+ ### API Consistency
95
+
96
+ The `buildFormSchemas()` function provides the same return type as `@formspec/build`:
97
+
98
+ | DSL | Function | Returns |
99
+ |-----|----------|---------|
100
+ | Chain DSL | `buildFormSchemas(form)` | `{ jsonSchema, uiSchema }` |
101
+ | Decorator DSL | `buildFormSchemas(Class)` | `{ jsonSchema, uiSchema }` |
102
+
103
+ This allows you to switch between DSLs without changing how you consume the schemas.
104
+
105
+ ## How It Works
106
+
107
+ These decorators are **no-ops at runtime** - they have zero overhead in your production code.
108
+
109
+ The FormSpec CLI uses TypeScript's compiler API to statically analyze your source files. It reads decorator names and arguments directly from the AST, without ever executing your code.
110
+
111
+ This means:
112
+ - No reflection metadata required
113
+ - No runtime dependencies
114
+ - Works with any TypeScript configuration
115
+ - Tree-shaking friendly
116
+
117
+ ## Available Decorators
118
+
119
+ ### Field Metadata
120
+
121
+ | Decorator | Purpose | Example |
122
+ |-----------|---------|---------|
123
+ | `@Label(text)` | Display label | `@Label("Full Name")` |
124
+ | `@Placeholder(text)` | Input placeholder | `@Placeholder("Enter name...")` |
125
+ | `@Description(text)` | Help text | `@Description("Your legal name")` |
126
+
127
+ ### Numeric Constraints
128
+
129
+ | Decorator | Purpose | Example |
130
+ |-----------|---------|---------|
131
+ | `@Min(n)` | Minimum value | `@Min(0)` |
132
+ | `@Max(n)` | Maximum value | `@Max(100)` |
133
+ | `@Step(n)` | Step increment | `@Step(0.01)` |
134
+
135
+ ### String Constraints
136
+
137
+ | Decorator | Purpose | Example |
138
+ |-----------|---------|---------|
139
+ | `@MinLength(n)` | Minimum length | `@MinLength(1)` |
140
+ | `@MaxLength(n)` | Maximum length | `@MaxLength(255)` |
141
+ | `@Pattern(regex)` | Regex pattern | `@Pattern("^[a-z]+$")` |
142
+
143
+ ### Array Constraints
144
+
145
+ | Decorator | Purpose | Example |
146
+ |-----------|---------|---------|
147
+ | `@MinItems(n)` | Minimum items | `@MinItems(1)` |
148
+ | `@MaxItems(n)` | Maximum items | `@MaxItems(10)` |
149
+
150
+ ### Enum Options
151
+
152
+ | Decorator | Purpose | Example |
153
+ |-----------|---------|---------|
154
+ | `@EnumOptions(opts)` | Custom labels | `@EnumOptions([{id: "us", label: "USA"}])` |
155
+
156
+ ### Layout & Conditional
157
+
158
+ | Decorator | Purpose | Example |
159
+ |-----------|---------|---------|
160
+ | `@Group(name)` | Group fields | `@Group("Contact Info")` |
161
+ | `@ShowWhen(cond)` | Conditional visibility | `@ShowWhen({ field: "type", value: "other" })` |
162
+
163
+ ## TypeScript Configuration
164
+
165
+ For decorator support, ensure your `tsconfig.json` includes:
166
+
167
+ ```json
168
+ {
169
+ "compilerOptions": {
170
+ "experimentalDecorators": true
171
+ }
172
+ }
173
+ ```
174
+
175
+ Note: The `emitDecoratorMetadata` flag is not required since these decorators don't use reflection.
176
+
177
+ ## License
178
+
179
+ UNLICENSED
package/dist/index.cjs ADDED
@@ -0,0 +1,380 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // dist/index.js
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ Description: () => Description,
24
+ EnumOptions: () => EnumOptions,
25
+ Group: () => Group,
26
+ Label: () => Label,
27
+ Max: () => Max,
28
+ MaxItems: () => MaxItems,
29
+ MaxLength: () => MaxLength,
30
+ Min: () => Min,
31
+ MinItems: () => MinItems,
32
+ MinLength: () => MinLength,
33
+ Pattern: () => Pattern,
34
+ Placeholder: () => Placeholder,
35
+ ShowWhen: () => ShowWhen,
36
+ Step: () => Step,
37
+ buildFormSchemas: () => buildFormSchemas,
38
+ getDecoratorMetadata: () => getDecoratorMetadata,
39
+ getTypeMetadata: () => getTypeMetadata,
40
+ toFormSpec: () => toFormSpec
41
+ });
42
+ module.exports = __toCommonJS(index_exports);
43
+ var decoratorMetadata = /* @__PURE__ */ new Map();
44
+ function getFieldMetadata(ctor, propertyKey) {
45
+ if (!decoratorMetadata.has(ctor)) {
46
+ decoratorMetadata.set(ctor, /* @__PURE__ */ new Map());
47
+ }
48
+ const classMetadata = decoratorMetadata.get(ctor);
49
+ if (!classMetadata.has(propertyKey)) {
50
+ classMetadata.set(propertyKey, {});
51
+ }
52
+ return classMetadata.get(propertyKey);
53
+ }
54
+ function Label(text) {
55
+ return function(target, propertyKey) {
56
+ const ctor = target.constructor;
57
+ getFieldMetadata(ctor, propertyKey).label = text;
58
+ };
59
+ }
60
+ function Placeholder(text) {
61
+ return function(target, propertyKey) {
62
+ const ctor = target.constructor;
63
+ getFieldMetadata(ctor, propertyKey).placeholder = text;
64
+ };
65
+ }
66
+ function Description(text) {
67
+ return function(target, propertyKey) {
68
+ const ctor = target.constructor;
69
+ getFieldMetadata(ctor, propertyKey).description = text;
70
+ };
71
+ }
72
+ function Min(value) {
73
+ return function(target, propertyKey) {
74
+ const ctor = target.constructor;
75
+ getFieldMetadata(ctor, propertyKey).min = value;
76
+ };
77
+ }
78
+ function Max(value) {
79
+ return function(target, propertyKey) {
80
+ const ctor = target.constructor;
81
+ getFieldMetadata(ctor, propertyKey).max = value;
82
+ };
83
+ }
84
+ function Step(value) {
85
+ return function(target, propertyKey) {
86
+ const ctor = target.constructor;
87
+ getFieldMetadata(ctor, propertyKey).step = value;
88
+ };
89
+ }
90
+ function MinLength(value) {
91
+ return function(target, propertyKey) {
92
+ const ctor = target.constructor;
93
+ getFieldMetadata(ctor, propertyKey).minLength = value;
94
+ };
95
+ }
96
+ function MaxLength(value) {
97
+ return function(target, propertyKey) {
98
+ const ctor = target.constructor;
99
+ getFieldMetadata(ctor, propertyKey).maxLength = value;
100
+ };
101
+ }
102
+ function Pattern(regex) {
103
+ return function(target, propertyKey) {
104
+ const ctor = target.constructor;
105
+ getFieldMetadata(ctor, propertyKey).pattern = regex;
106
+ };
107
+ }
108
+ function MinItems(value) {
109
+ return function(target, propertyKey) {
110
+ const ctor = target.constructor;
111
+ getFieldMetadata(ctor, propertyKey).minItems = value;
112
+ };
113
+ }
114
+ function MaxItems(value) {
115
+ return function(target, propertyKey) {
116
+ const ctor = target.constructor;
117
+ getFieldMetadata(ctor, propertyKey).maxItems = value;
118
+ };
119
+ }
120
+ function EnumOptions(options) {
121
+ return function(target, propertyKey) {
122
+ const ctor = target.constructor;
123
+ getFieldMetadata(ctor, propertyKey).options = options;
124
+ };
125
+ }
126
+ function ShowWhen(condition) {
127
+ return function(target, propertyKey) {
128
+ const ctor = target.constructor;
129
+ getFieldMetadata(ctor, propertyKey).showWhen = condition;
130
+ };
131
+ }
132
+ function Group(name) {
133
+ return function(target, propertyKey) {
134
+ const ctor = target.constructor;
135
+ getFieldMetadata(ctor, propertyKey).group = name;
136
+ };
137
+ }
138
+ var FORMSPEC_TYPES_KEY = "__formspec_types__";
139
+ function mapTypeToFieldType(type) {
140
+ switch (type) {
141
+ case "string":
142
+ return "text";
143
+ case "number":
144
+ return "number";
145
+ case "boolean":
146
+ return "boolean";
147
+ case "enum":
148
+ return "enum";
149
+ case "array":
150
+ return "array";
151
+ case "object":
152
+ return "object";
153
+ default:
154
+ return "text";
155
+ }
156
+ }
157
+ function createField(fieldName, typeInfo, decoratorInfo) {
158
+ const field = {
159
+ _field: mapTypeToFieldType(typeInfo.type),
160
+ id: fieldName
161
+ };
162
+ if (!typeInfo.optional && !typeInfo.nullable) {
163
+ field.required = true;
164
+ }
165
+ if (decoratorInfo.label)
166
+ field.label = decoratorInfo.label;
167
+ if (decoratorInfo.placeholder)
168
+ field.placeholder = decoratorInfo.placeholder;
169
+ if (decoratorInfo.description)
170
+ field.description = decoratorInfo.description;
171
+ if (decoratorInfo.min !== void 0)
172
+ field.min = decoratorInfo.min;
173
+ if (decoratorInfo.max !== void 0)
174
+ field.max = decoratorInfo.max;
175
+ if (decoratorInfo.step !== void 0)
176
+ field.step = decoratorInfo.step;
177
+ if (decoratorInfo.minLength !== void 0)
178
+ field.minLength = decoratorInfo.minLength;
179
+ if (decoratorInfo.maxLength !== void 0)
180
+ field.maxLength = decoratorInfo.maxLength;
181
+ if (decoratorInfo.minItems !== void 0)
182
+ field.minItems = decoratorInfo.minItems;
183
+ if (decoratorInfo.maxItems !== void 0)
184
+ field.maxItems = decoratorInfo.maxItems;
185
+ if (decoratorInfo.pattern)
186
+ field.pattern = decoratorInfo.pattern;
187
+ if (decoratorInfo.showWhen)
188
+ field.showWhen = decoratorInfo.showWhen;
189
+ if (decoratorInfo.group)
190
+ field.group = decoratorInfo.group;
191
+ if (decoratorInfo.options) {
192
+ field.options = decoratorInfo.options;
193
+ } else if (typeInfo.values) {
194
+ field.options = typeInfo.values.map((v) => String(v));
195
+ }
196
+ if (typeInfo.type === "object" && typeInfo.properties) {
197
+ field.fields = Object.entries(typeInfo.properties).map(([propName, propType]) => createField(propName, propType, {}));
198
+ }
199
+ return field;
200
+ }
201
+ function toFormSpec(ctor) {
202
+ const typeMetadata = (
203
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
204
+ ctor[FORMSPEC_TYPES_KEY] ?? {}
205
+ );
206
+ const classDecoratorMeta = decoratorMetadata.get(ctor) ?? /* @__PURE__ */ new Map();
207
+ const elements = [];
208
+ for (const [fieldName, typeInfo] of Object.entries(typeMetadata)) {
209
+ const decoratorInfo = classDecoratorMeta.get(fieldName) ?? {};
210
+ elements.push(createField(fieldName, typeInfo, decoratorInfo));
211
+ }
212
+ if (Object.keys(typeMetadata).length === 0) {
213
+ for (const [fieldName, decoratorInfo] of classDecoratorMeta.entries()) {
214
+ elements.push(createField(fieldName, { type: "unknown" }, decoratorInfo));
215
+ }
216
+ }
217
+ return { elements };
218
+ }
219
+ function getDecoratorMetadata(ctor) {
220
+ return decoratorMetadata.get(ctor) ?? /* @__PURE__ */ new Map();
221
+ }
222
+ function getTypeMetadata(ctor) {
223
+ return ctor[FORMSPEC_TYPES_KEY] ?? {};
224
+ }
225
+ function fieldTypeToJsonSchemaType(fieldType) {
226
+ switch (fieldType) {
227
+ case "text":
228
+ return "string";
229
+ case "number":
230
+ return "number";
231
+ case "boolean":
232
+ return "boolean";
233
+ case "enum":
234
+ return "string";
235
+ case "array":
236
+ return "array";
237
+ case "object":
238
+ return "object";
239
+ default:
240
+ return "string";
241
+ }
242
+ }
243
+ function fieldToJsonSchema(field) {
244
+ const schema = {};
245
+ if (field.label) {
246
+ schema.title = field.label;
247
+ }
248
+ const jsonType = fieldTypeToJsonSchemaType(field._field);
249
+ schema.type = jsonType;
250
+ if (field.min !== void 0)
251
+ schema.minimum = field.min;
252
+ if (field.max !== void 0)
253
+ schema.maximum = field.max;
254
+ if (field.minLength !== void 0)
255
+ schema.minLength = field.minLength;
256
+ if (field.maxLength !== void 0)
257
+ schema.maxLength = field.maxLength;
258
+ if (field.pattern)
259
+ schema.pattern = field.pattern;
260
+ if (field._field === "enum" && field.options) {
261
+ const hasLabels = field.options.some((opt) => typeof opt === "object" && opt !== null && "id" in opt);
262
+ if (hasLabels) {
263
+ schema.oneOf = field.options.map((opt) => {
264
+ if (typeof opt === "object" && "id" in opt) {
265
+ return { const: opt.id, title: opt.label };
266
+ }
267
+ return { const: opt, title: String(opt) };
268
+ });
269
+ } else {
270
+ schema.enum = field.options.map((opt) => typeof opt === "string" ? opt : opt.id);
271
+ }
272
+ }
273
+ if (field._field === "array" && field.fields) {
274
+ const itemProperties = {};
275
+ const itemRequired = [];
276
+ for (const nested of field.fields) {
277
+ itemProperties[nested.id] = fieldToJsonSchema(nested);
278
+ if (nested.required)
279
+ itemRequired.push(nested.id);
280
+ }
281
+ schema.items = {
282
+ type: "object",
283
+ properties: itemProperties,
284
+ ...itemRequired.length > 0 && { required: itemRequired }
285
+ };
286
+ if (field.minItems !== void 0)
287
+ schema.minItems = field.minItems;
288
+ if (field.maxItems !== void 0)
289
+ schema.maxItems = field.maxItems;
290
+ }
291
+ if (field._field === "object" && field.fields) {
292
+ const objProperties = {};
293
+ const objRequired = [];
294
+ for (const nested of field.fields) {
295
+ objProperties[nested.id] = fieldToJsonSchema(nested);
296
+ if (nested.required)
297
+ objRequired.push(nested.id);
298
+ }
299
+ schema.properties = objProperties;
300
+ if (objRequired.length > 0)
301
+ schema.required = objRequired;
302
+ }
303
+ return schema;
304
+ }
305
+ function generateJsonSchemaFromElements(elements) {
306
+ const properties = {};
307
+ const required = [];
308
+ for (const element of elements) {
309
+ properties[element.id] = fieldToJsonSchema(element);
310
+ if (element.required) {
311
+ required.push(element.id);
312
+ }
313
+ }
314
+ return {
315
+ $schema: "https://json-schema.org/draft-07/schema#",
316
+ type: "object",
317
+ properties,
318
+ ...required.length > 0 && { required }
319
+ };
320
+ }
321
+ function fieldToScope(fieldName) {
322
+ return `#/properties/${fieldName}`;
323
+ }
324
+ function elementsToUiSchema(elements) {
325
+ const result = [];
326
+ for (const element of elements) {
327
+ const control = {
328
+ type: "Control",
329
+ scope: fieldToScope(element.id)
330
+ };
331
+ if (element.label) {
332
+ control.label = element.label;
333
+ }
334
+ if (element.showWhen) {
335
+ control.rule = {
336
+ effect: "SHOW",
337
+ condition: {
338
+ scope: fieldToScope(element.showWhen.field),
339
+ schema: { const: element.showWhen.value }
340
+ }
341
+ };
342
+ }
343
+ result.push(control);
344
+ }
345
+ return result;
346
+ }
347
+ function generateUiSchemaFromElements(elements) {
348
+ return {
349
+ type: "VerticalLayout",
350
+ elements: elementsToUiSchema(elements)
351
+ };
352
+ }
353
+ function buildFormSchemas(ctor) {
354
+ const { elements } = toFormSpec(ctor);
355
+ return {
356
+ jsonSchema: generateJsonSchemaFromElements(elements),
357
+ uiSchema: generateUiSchemaFromElements(elements)
358
+ };
359
+ }
360
+ // Annotate the CommonJS export names for ESM import in node:
361
+ 0 && (module.exports = {
362
+ Description,
363
+ EnumOptions,
364
+ Group,
365
+ Label,
366
+ Max,
367
+ MaxItems,
368
+ MaxLength,
369
+ Min,
370
+ MinItems,
371
+ MinLength,
372
+ Pattern,
373
+ Placeholder,
374
+ ShowWhen,
375
+ Step,
376
+ buildFormSchemas,
377
+ getDecoratorMetadata,
378
+ getTypeMetadata,
379
+ toFormSpec
380
+ });