@formspec/dsl 0.1.0-alpha.0 → 0.1.0-alpha.11

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,343 @@
1
+ # @formspec/dsl
2
+
3
+ Type-safe form definition using a fluent builder API. This is the recommended approach for defining forms programmatically, especially when you need runtime form construction or dynamic forms.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @formspec/dsl @formspec/build
9
+ # Or use the umbrella package:
10
+ npm install formspec
11
+ ```
12
+
13
+ ## Requirements
14
+
15
+ This package is ESM-only and requires:
16
+
17
+ ```json
18
+ // package.json
19
+ {
20
+ "type": "module"
21
+ }
22
+ ```
23
+
24
+ ```json
25
+ // tsconfig.json
26
+ {
27
+ "compilerOptions": {
28
+ "module": "NodeNext",
29
+ "moduleResolution": "NodeNext"
30
+ }
31
+ }
32
+ ```
33
+
34
+ ## Quick Start
35
+
36
+ ```typescript
37
+ import { formspec, field, group, when, is, type InferFormSchema } from "@formspec/dsl";
38
+ import { buildFormSchemas } from "@formspec/build";
39
+
40
+ // Define a form with full type safety
41
+ const ContactForm = formspec(
42
+ field.text("name", { label: "Name", required: true }),
43
+ field.text("email", { label: "Email", required: true }),
44
+ field.enum("subject", ["general", "support", "sales"] as const, {
45
+ label: "Subject",
46
+ required: true,
47
+ }),
48
+ field.text("message", { label: "Message", required: true }),
49
+ field.boolean("subscribe", { label: "Subscribe to newsletter" })
50
+ );
51
+
52
+ // Infer TypeScript types from the form definition
53
+ type ContactData = InferFormSchema<typeof ContactForm>;
54
+ // Result: { name: string; email: string; subject: "general" | "support" | "sales"; message: string; subscribe: boolean }
55
+
56
+ // Generate JSON Schema and UI Schema
57
+ const { jsonSchema, uiSchema } = buildFormSchemas(ContactForm);
58
+ ```
59
+
60
+ ## Field Types
61
+
62
+ ### Text Field
63
+
64
+ ```typescript
65
+ field.text("fieldName", {
66
+ label: "Display Label",
67
+ description: "Help text",
68
+ required: true,
69
+ minLength: 1,
70
+ maxLength: 100,
71
+ pattern: "^[a-zA-Z]+$", // Regex pattern
72
+ });
73
+ ```
74
+
75
+ ### Number Field
76
+
77
+ ```typescript
78
+ field.number("age", {
79
+ label: "Age",
80
+ required: true,
81
+ min: 0,
82
+ max: 120,
83
+ });
84
+ ```
85
+
86
+ ### Boolean Field
87
+
88
+ ```typescript
89
+ field.boolean("acceptTerms", {
90
+ label: "I accept the terms and conditions",
91
+ required: true,
92
+ });
93
+ ```
94
+
95
+ ### Enum Field (Dropdown/Select)
96
+
97
+ Use `as const` to preserve literal types for type inference:
98
+
99
+ ```typescript
100
+ // Simple string options
101
+ field.enum("status", ["draft", "published", "archived"] as const, {
102
+ label: "Status",
103
+ required: true,
104
+ });
105
+
106
+ // Options with separate IDs and labels
107
+ field.enum(
108
+ "country",
109
+ [
110
+ { id: "us", label: "United States" },
111
+ { id: "ca", label: "Canada" },
112
+ { id: "uk", label: "United Kingdom" },
113
+ ] as const,
114
+ {
115
+ label: "Country",
116
+ }
117
+ );
118
+ ```
119
+
120
+ **Note:** Use `as const` when passing enum options from a variable. For inline array literals, the `const` type parameter preserves literal types automatically.
121
+
122
+ ### Dynamic Enum Field
123
+
124
+ For dropdowns populated at runtime from an API:
125
+
126
+ ```typescript
127
+ field.dynamicEnum("customerId", "fetch_customers", {
128
+ label: "Customer",
129
+ required: true,
130
+ });
131
+ ```
132
+
133
+ The second argument is a source identifier used with `@formspec/runtime` resolvers.
134
+
135
+ ### Array Field
136
+
137
+ For repeatable field groups:
138
+
139
+ ```typescript
140
+ field.array(
141
+ "lineItems",
142
+ field.text("description", { label: "Description", required: true }),
143
+ field.number("quantity", { label: "Quantity", min: 1 }),
144
+ field.number("price", { label: "Unit Price", min: 0 })
145
+ );
146
+
147
+ // With configuration
148
+ field.arrayWithConfig(
149
+ "contacts",
150
+ { label: "Contact List", minItems: 1, maxItems: 5 },
151
+ field.text("name", { label: "Name" }),
152
+ field.text("phone", { label: "Phone" })
153
+ );
154
+ ```
155
+
156
+ ### Object Field
157
+
158
+ For nested field groups:
159
+
160
+ ```typescript
161
+ field.object(
162
+ "address",
163
+ field.text("street", { label: "Street", required: true }),
164
+ field.text("city", { label: "City", required: true }),
165
+ field.text("zipCode", { label: "ZIP Code", required: true })
166
+ );
167
+ ```
168
+
169
+ ## Grouping
170
+
171
+ Use `group()` to visually organize fields:
172
+
173
+ ```typescript
174
+ const UserForm = formspec(
175
+ group(
176
+ "Personal Information",
177
+ field.text("firstName", { label: "First Name", required: true }),
178
+ field.text("lastName", { label: "Last Name", required: true }),
179
+ field.text("email", { label: "Email", required: true })
180
+ ),
181
+ group(
182
+ "Preferences",
183
+ field.enum("theme", ["light", "dark", "system"] as const, { label: "Theme" }),
184
+ field.boolean("notifications", { label: "Enable notifications" })
185
+ )
186
+ );
187
+ ```
188
+
189
+ ## Conditional Fields
190
+
191
+ Use `when()` and `is()` to show/hide fields based on other field values:
192
+
193
+ ```typescript
194
+ const OrderForm = formspec(
195
+ field.enum("shippingMethod", ["standard", "express", "pickup"] as const, {
196
+ label: "Shipping Method",
197
+ required: true,
198
+ }),
199
+
200
+ // Only show address fields when shipping method is not "pickup"
201
+ when(
202
+ is("shippingMethod", "standard"),
203
+ field.text("address", { label: "Shipping Address", required: true }),
204
+ field.text("city", { label: "City", required: true })
205
+ ),
206
+ when(
207
+ is("shippingMethod", "express"),
208
+ field.text("address", { label: "Shipping Address", required: true }),
209
+ field.text("city", { label: "City", required: true }),
210
+ field.text("phone", { label: "Phone for courier", required: true })
211
+ )
212
+ );
213
+ ```
214
+
215
+ ## Type Inference
216
+
217
+ The library provides powerful type inference utilities:
218
+
219
+ ```typescript
220
+ import { type InferFormSchema, type InferFieldValue } from "@formspec/dsl";
221
+
222
+ const MyForm = formspec(
223
+ field.text("name"),
224
+ field.number("age"),
225
+ field.enum("role", ["admin", "user", "guest"] as const)
226
+ );
227
+
228
+ // Infer the complete form data type
229
+ type FormData = InferFormSchema<typeof MyForm>;
230
+ // { name: string; age: number; role: "admin" | "user" | "guest" }
231
+
232
+ // Access form elements at runtime
233
+ for (const element of MyForm.elements) {
234
+ if (element._type === "field") {
235
+ console.log(element.name, element._field);
236
+ }
237
+ }
238
+ ```
239
+
240
+ ## Validation
241
+
242
+ Validate form definitions at runtime:
243
+
244
+ ```typescript
245
+ import { formspec, field, validateForm, logValidationIssues } from "@formspec/dsl";
246
+
247
+ const form = formspec(
248
+ field.text("email"),
249
+ field.text("email") // Duplicate field name!
250
+ );
251
+
252
+ const result = validateForm(form.elements);
253
+ if (!result.valid) {
254
+ logValidationIssues(result, "MyForm");
255
+ // Logs: [MyForm] ERROR at email: Duplicate field name "email"
256
+ }
257
+
258
+ // Or use formspecWithValidation for automatic checking
259
+ import { formspecWithValidation } from "@formspec/dsl";
260
+
261
+ const validatedForm = formspecWithValidation(
262
+ { name: "MyForm", validate: "throw" },
263
+ field.text("email"),
264
+ field.text("email") // Throws error!
265
+ );
266
+ ```
267
+
268
+ ## Schema Generation
269
+
270
+ Use `@formspec/build` to generate JSON Schema and UI Schema:
271
+
272
+ ```typescript
273
+ import { buildFormSchemas, writeSchemas } from "@formspec/build";
274
+
275
+ // Get schema objects
276
+ const { jsonSchema, uiSchema } = buildFormSchemas(MyForm);
277
+
278
+ // Or write to files
279
+ writeSchemas(MyForm, {
280
+ outDir: "./generated",
281
+ name: "MyForm",
282
+ });
283
+ // Creates:
284
+ // ./generated/MyForm-schema.json
285
+ // ./generated/MyForm-uischema.json
286
+ ```
287
+
288
+ ## When to Use This Package
289
+
290
+ Use `@formspec/dsl` when:
291
+
292
+ - **Forms are defined programmatically** - Building forms from configuration or code
293
+ - **Runtime form construction** - Creating forms dynamically based on user input or API data
294
+ - **Full type inference needed** - Deriving TypeScript types from form definitions
295
+ - **No build step preferred** - Works directly at runtime without CLI codegen
296
+
297
+ Consider `@formspec/decorators` when:
298
+
299
+ - **Class-based forms preferred** - Using TypeScript classes with property decorators
300
+ - **Type inference from existing types** - Leveraging existing TypeScript class types
301
+ - **Static analysis available** - Using the CLI for build-time schema generation
302
+
303
+ ## API Reference
304
+
305
+ ### Functions
306
+
307
+ | Function | Description |
308
+ | ---------------------------------------------- | -------------------------------- |
309
+ | `formspec(...elements)` | Create a form specification |
310
+ | `formspecWithValidation(options, ...elements)` | Create a form with validation |
311
+ | `group(label, ...elements)` | Create a visual field group |
312
+ | `when(predicate, ...elements)` | Create conditional fields |
313
+ | `is(fieldName, value)` | Create an equality predicate |
314
+ | `validateForm(elements)` | Validate form elements |
315
+ | `logValidationIssues(result)` | Log validation issues to console |
316
+
317
+ ### Field Builders
318
+
319
+ | Builder | Description |
320
+ | ----------------------------------------------------- | ------------------------- |
321
+ | `field.text(name, config?)` | Text input field |
322
+ | `field.number(name, config?)` | Numeric input field |
323
+ | `field.boolean(name, config?)` | Checkbox/toggle field |
324
+ | `field.enum(name, options, config?)` | Dropdown/select field |
325
+ | `field.dynamicEnum(name, source, config?)` | API-populated dropdown |
326
+ | `field.dynamicSchema(name, source, config?)` | Dynamic nested schema |
327
+ | `field.array(name, ...items)` | Repeatable field array |
328
+ | `field.arrayWithConfig(name, config, ...items)` | Array with configuration |
329
+ | `field.object(name, ...properties)` | Nested object field |
330
+ | `field.objectWithConfig(name, config, ...properties)` | Object with configuration |
331
+
332
+ ### Type Utilities
333
+
334
+ | Type | Description |
335
+ | ----------------------- | ------------------------------------ |
336
+ | `InferFormSchema<F>` | Infer data type from FormSpec |
337
+ | `InferSchema<Elements>` | Infer data type from element array |
338
+ | `InferFieldValue<F>` | Infer value type from a single field |
339
+ | `ExtractFields<E>` | Extract all fields from an element |
340
+
341
+ ## License
342
+
343
+ UNLICENSED