@formos/kernel 0.1.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/README.md +433 -0
- package/dist/index.d.ts +372 -0
- package/dist/index.js +763 -0
- package/dist/index.js.map +1 -0
- package/package.json +60 -0
package/README.md
ADDED
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
# @formos/kernel
|
|
2
|
+
|
|
3
|
+
Headless, framework-agnostic form engine for the Formos form builder.
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
`@formos/kernel` provides the **runtime execution layer** for forms defined by `@formos/schema`. It's a completely headless engine that manages all form logic without any UI concerns - no React, no DOM, no browser APIs.
|
|
8
|
+
|
|
9
|
+
Think of it as the **"brain"** of the form - it handles state, validation orchestration, effect execution, and multi-step navigation, but leaves rendering entirely to UI packages.
|
|
10
|
+
|
|
11
|
+
## Responsibilities
|
|
12
|
+
|
|
13
|
+
This package is responsible for:
|
|
14
|
+
|
|
15
|
+
- **State Management**: Maintaining form values, errors, touched fields, and dirty state
|
|
16
|
+
- **Validation Orchestration**: Coordinating validation execution with pluggable validator adapters
|
|
17
|
+
- **Effect Execution**: Triggering side effects (field syncing, API calls) via effect executors
|
|
18
|
+
- **Conditional Logic Evaluation**: Determining field visibility and required status dynamically
|
|
19
|
+
- **Multi-Step Flow Control**: Managing step navigation with validation guards
|
|
20
|
+
- **Lifecycle Management**: Handling submit, reset, and field value updates
|
|
21
|
+
|
|
22
|
+
## What This Package Does NOT Do
|
|
23
|
+
|
|
24
|
+
❌ **NO UI Rendering**
|
|
25
|
+
- Does not create DOM elements
|
|
26
|
+
- Does not provide React components
|
|
27
|
+
- Does not handle user interactions directly
|
|
28
|
+
|
|
29
|
+
❌ **NO Framework Dependencies**
|
|
30
|
+
- No React, Vue, Svelte, or any UI framework
|
|
31
|
+
- No browser-specific APIs (localStorage, fetch, etc.)
|
|
32
|
+
- Completely runtime-agnostic
|
|
33
|
+
|
|
34
|
+
❌ **NO Built-in Validation Libraries**
|
|
35
|
+
- Does not include Zod, Yup, or any validators
|
|
36
|
+
- Uses **adapter pattern** - you provide validators
|
|
37
|
+
- Same for effects - no built-in API clients or transformers
|
|
38
|
+
|
|
39
|
+
❌ **NO Schema Definition**
|
|
40
|
+
- Does not define schema types (that's `@formos/schema`)
|
|
41
|
+
- Consumes `NormalizedSchemaV1` as input
|
|
42
|
+
- Does not modify or mutate schemas
|
|
43
|
+
|
|
44
|
+
**Think of it as:** The engine that executes the schema's instructions, not the schema itself.
|
|
45
|
+
|
|
46
|
+
## Stability Guarantee
|
|
47
|
+
|
|
48
|
+
### FormEngine API is STABLE and FINAL ✅
|
|
49
|
+
|
|
50
|
+
The `FormEngine` interface is committed to backward compatibility:
|
|
51
|
+
- Method signatures won't change
|
|
52
|
+
- Return types remain consistent
|
|
53
|
+
- Behavior is predictable and documented
|
|
54
|
+
|
|
55
|
+
This guarantee enables:
|
|
56
|
+
- **Safe updates**: Upgrade the kernel without UI changes
|
|
57
|
+
- **Framework flexibility**: Build UI packages for React, Vue, Svelte
|
|
58
|
+
- **Long-term reliability**: Code written today works tomorrow
|
|
59
|
+
|
|
60
|
+
### What Can Change
|
|
61
|
+
|
|
62
|
+
✅ **Internal implementations** can be optimized
|
|
63
|
+
✅ **New optional methods** may be added to `FormEngine`
|
|
64
|
+
✅ **Performance improvements** without API changes
|
|
65
|
+
✅ **Bug fixes** that don't alter public behavior
|
|
66
|
+
|
|
67
|
+
### Adapter Contract
|
|
68
|
+
|
|
69
|
+
The adapter types (`ValidatorExecutor`, `EffectExecutor`) are **stable**:
|
|
70
|
+
- Validators return `string | null` (error or valid)
|
|
71
|
+
- Effects receive context with `getValue` and `setValue`
|
|
72
|
+
- These interfaces won't have breaking changes
|
|
73
|
+
|
|
74
|
+
## Installation
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
pnpm add @formos/kernel @formos/schema
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Basic Usage
|
|
81
|
+
|
|
82
|
+
### Create a Form Engine
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
import { createFormEngine } from '@formos/kernel';
|
|
86
|
+
import { normalizeSchema, type FormSchemaV1 } from '@formos/schema';
|
|
87
|
+
|
|
88
|
+
// 1. Define schema
|
|
89
|
+
const schema: FormSchemaV1 = {
|
|
90
|
+
version: 'v1',
|
|
91
|
+
fields: [
|
|
92
|
+
{
|
|
93
|
+
name: 'email',
|
|
94
|
+
type: 'email',
|
|
95
|
+
required: true,
|
|
96
|
+
validations: [
|
|
97
|
+
{
|
|
98
|
+
trigger: ['onChange', 'onBlur'],
|
|
99
|
+
validator: 'email',
|
|
100
|
+
rule: {},
|
|
101
|
+
message: 'Invalid email'
|
|
102
|
+
}
|
|
103
|
+
]
|
|
104
|
+
},
|
|
105
|
+
{ name: 'password', type: 'password', required: true }
|
|
106
|
+
]
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// 2. Normalize schema
|
|
110
|
+
const normalized = normalizeSchema(schema);
|
|
111
|
+
|
|
112
|
+
// 3. Create engine with validators
|
|
113
|
+
const engine = createFormEngine(normalized, {
|
|
114
|
+
validators: {
|
|
115
|
+
required: (value) => value ? null : 'Required',
|
|
116
|
+
email: (value) => {
|
|
117
|
+
const regex = /\S+@\S+\.\S+/;
|
|
118
|
+
return regex.test(String(value)) ? null : 'Invalid email';
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
onSubmit: async (values) => {
|
|
122
|
+
console.log('Form submitted:', values);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Get and Set Values
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
// Get field value
|
|
131
|
+
const email = engine.getValue('email');
|
|
132
|
+
console.log('Email:', email); // undefined initially
|
|
133
|
+
|
|
134
|
+
// Set field value (triggers validation + effects)
|
|
135
|
+
await engine.setValue('email', 'user@example.com');
|
|
136
|
+
|
|
137
|
+
// Set without validation
|
|
138
|
+
await engine.setValue('email', 'user@example.com', {
|
|
139
|
+
trigger: 'manual',
|
|
140
|
+
skipValidation: true
|
|
141
|
+
});
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Validation
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
// Validate single field
|
|
148
|
+
const isEmailValid = await engine.validate('email', 'onChange');
|
|
149
|
+
|
|
150
|
+
// Validate entire form
|
|
151
|
+
const isFormValid = await engine.validate();
|
|
152
|
+
|
|
153
|
+
// Check validation state
|
|
154
|
+
const emailError = engine.getError('email');
|
|
155
|
+
const allErrors = engine.getErrors();
|
|
156
|
+
const isValid = engine.isValid();
|
|
157
|
+
|
|
158
|
+
// Manual error management
|
|
159
|
+
engine.setError('email', 'Custom error message');
|
|
160
|
+
engine.clearError('email');
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Form State
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
// Check if field was modified
|
|
167
|
+
const isEmailDirty = engine.isDirty('email');
|
|
168
|
+
const isFormDirty = engine.isDirty(); // Any field dirty?
|
|
169
|
+
|
|
170
|
+
// Check if field was touched (focused and blurred)
|
|
171
|
+
const wasEmailTouched = engine.isTouched('email');
|
|
172
|
+
|
|
173
|
+
// Mark field as touched
|
|
174
|
+
engine.markTouched('email');
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Submit and Reset
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
// Submit form (validates all fields first)
|
|
181
|
+
const success = await engine.submit();
|
|
182
|
+
if (success) {
|
|
183
|
+
console.log('Form submitted successfully');
|
|
184
|
+
} else {
|
|
185
|
+
console.log('Validation failed:', engine.getErrors());
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Reset form to initial state
|
|
189
|
+
engine.reset();
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Multi-Step Forms
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
const multiStepSchema: FormSchemaV1 = {
|
|
196
|
+
version: 'v1',
|
|
197
|
+
fields: [
|
|
198
|
+
{ name: 'firstName', type: 'text' },
|
|
199
|
+
{ name: 'lastName', type: 'text' },
|
|
200
|
+
{ name: 'email', type: 'email' },
|
|
201
|
+
{ name: 'phone', type: 'tel' }
|
|
202
|
+
],
|
|
203
|
+
steps: [
|
|
204
|
+
{ id: 'personal', title: 'Personal Info', fields: ['firstName', 'lastName'] },
|
|
205
|
+
{ id: 'contact', title: 'Contact', fields: ['email', 'phone'] }
|
|
206
|
+
]
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const normalized = normalizeSchema(multiStepSchema);
|
|
210
|
+
const engine = createFormEngine(normalized, { validators });
|
|
211
|
+
|
|
212
|
+
// Step navigation (only available for multi-step forms)
|
|
213
|
+
const currentStep = engine.getCurrentStep?.(); // 0
|
|
214
|
+
const totalSteps = engine.getTotalSteps?.(); // 2
|
|
215
|
+
|
|
216
|
+
// Go to next step (validates current step first)
|
|
217
|
+
const canProceed = await engine.nextStep?.();
|
|
218
|
+
if (!canProceed) {
|
|
219
|
+
console.log('Fix errors before proceeding');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Go to previous step (no validation)
|
|
223
|
+
engine.prevStep?.();
|
|
224
|
+
|
|
225
|
+
// Jump to specific step (validates current step first)
|
|
226
|
+
const moved = await engine.goToStep?.(1);
|
|
227
|
+
|
|
228
|
+
// Check if can go to step
|
|
229
|
+
const canGo = engine.canGoToStep?.(1);
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## Advanced: Custom Validators
|
|
233
|
+
|
|
234
|
+
Validators use the **adapter pattern** for framework flexibility:
|
|
235
|
+
|
|
236
|
+
```typescript
|
|
237
|
+
import type { ValidatorExecutor } from '@formos/kernel';
|
|
238
|
+
|
|
239
|
+
// Simple validator
|
|
240
|
+
const required: ValidatorExecutor = (value, params, context) => {
|
|
241
|
+
return value ? null : 'Required';
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
// Cross-field validator
|
|
245
|
+
const matchesField: ValidatorExecutor = (value, params, context) => {
|
|
246
|
+
const { targetField } = params as { targetField: string };
|
|
247
|
+
const targetValue = context.getValue(targetField);
|
|
248
|
+
return value === targetValue ? null : 'Fields must match';
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
// Async validator (e.g., API check)
|
|
252
|
+
const uniqueUsername: ValidatorExecutor = async (value, params, context) => {
|
|
253
|
+
const response = await fetch(`/api/check-username?username=${value}`);
|
|
254
|
+
const { available } = await response.json();
|
|
255
|
+
return available ? null : 'Username already taken';
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const engine = createFormEngine(normalized, {
|
|
259
|
+
validators: {
|
|
260
|
+
required,
|
|
261
|
+
matchesField,
|
|
262
|
+
uniqueUsername
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
## Advanced: Effect Executors
|
|
268
|
+
|
|
269
|
+
Effects handle side effects triggered by field changes:
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
import type { EffectExecutor } from '@formos/kernel';
|
|
273
|
+
|
|
274
|
+
// Sync effect: Calculate fullName from firstName + lastName
|
|
275
|
+
const syncFullName: EffectExecutor = (params, context) => {
|
|
276
|
+
const firstName = context.getValue('firstName') as string || '';
|
|
277
|
+
const lastName = context.getValue('lastName') as string || '';
|
|
278
|
+
context.setValue('fullName', `${firstName} ${lastName}`.trim());
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
// API effect: Fetch cities when country changes
|
|
282
|
+
const fetchCities: EffectExecutor = async (params, context) => {
|
|
283
|
+
const country = context.sourceValue as string;
|
|
284
|
+
|
|
285
|
+
const response = await fetch(`/api/cities?country=${country}`);
|
|
286
|
+
const cities = await response.json();
|
|
287
|
+
|
|
288
|
+
context.setValue('cities', cities);
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const engine = createFormEngine(normalized, {
|
|
292
|
+
effectExecutors: {
|
|
293
|
+
syncFullName,
|
|
294
|
+
fetchCities
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
## Conditional Logic
|
|
300
|
+
|
|
301
|
+
The kernel evaluates conditional visibility and required logic:
|
|
302
|
+
|
|
303
|
+
```typescript
|
|
304
|
+
import { evaluateConditional, isFieldVisible, isFieldRequired } from '@formos/kernel';
|
|
305
|
+
|
|
306
|
+
// Check field visibility
|
|
307
|
+
const field = schema.fieldMap?.get('ssn');
|
|
308
|
+
if (field && isFieldVisible(field, (name) => engine.getValue(name))) {
|
|
309
|
+
// Render SSN field
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Check if field is dynamically required
|
|
313
|
+
if (field && isFieldRequired(field, (name) => engine.getValue(name))) {
|
|
314
|
+
// Show required indicator
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Direct conditional evaluation
|
|
318
|
+
const condition = {
|
|
319
|
+
logic: 'and',
|
|
320
|
+
rules: [
|
|
321
|
+
{ field: 'age', operator: 'greaterThan', value: 18 },
|
|
322
|
+
{ field: 'country', operator: 'equals', value: 'US' }
|
|
323
|
+
]
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const result = evaluateConditional(condition, (name) => engine.getValue(name));
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
## API Reference
|
|
330
|
+
|
|
331
|
+
### Core Function
|
|
332
|
+
|
|
333
|
+
- **`createFormEngine(schema, options)`**: Create form engine instance
|
|
334
|
+
|
|
335
|
+
### FormEngine Interface
|
|
336
|
+
|
|
337
|
+
**State Accessors:**
|
|
338
|
+
- `getValue(fieldName)`: Get field value
|
|
339
|
+
- `getError(fieldName)`: Get field error
|
|
340
|
+
- `getErrors()`: Get all errors
|
|
341
|
+
- `isValid()`: Check if form is valid
|
|
342
|
+
- `isDirty(fieldName?)`: Check dirty state
|
|
343
|
+
- `isTouched(fieldName)`: Check touched state
|
|
344
|
+
|
|
345
|
+
**State Mutators:**
|
|
346
|
+
- `setValue(fieldName, value, options?)`: Set field value
|
|
347
|
+
- `setError(fieldName, error)`: Set field error
|
|
348
|
+
- `clearError(fieldName)`: Clear field error
|
|
349
|
+
- `markTouched(fieldName)`: Mark field as touched
|
|
350
|
+
|
|
351
|
+
**Validation:**
|
|
352
|
+
- `validate(fieldName?, trigger?)`: Validate field(s)
|
|
353
|
+
|
|
354
|
+
**Lifecycle:**
|
|
355
|
+
- `submit()`: Submit form (validates first)
|
|
356
|
+
- `reset()`: Reset form to initial state
|
|
357
|
+
|
|
358
|
+
**Multi-Step Navigation (if applicable):**
|
|
359
|
+
- `getCurrentStep?()`: Get current step index
|
|
360
|
+
- `getTotalSteps?()`: Get total step count
|
|
361
|
+
- `nextStep?()`: Go to next step (validates first)
|
|
362
|
+
- `prevStep?()`: Go to previous step
|
|
363
|
+
- `goToStep?(stepIndex)`: Jump to specific step
|
|
364
|
+
- `canGoToStep?(stepIndex)`: Check if can navigate to step
|
|
365
|
+
|
|
366
|
+
### Adapter Types
|
|
367
|
+
|
|
368
|
+
- **`ValidatorExecutor`**: `(value, params, context) => string | null | Promise<string | null>`
|
|
369
|
+
- **`EffectExecutor`**: `(params, context) => unknown | Promise<unknown>`
|
|
370
|
+
- **`ValidationContext`**: Read-only access to form values
|
|
371
|
+
- **`EffectContext`**: Read-write access to form values
|
|
372
|
+
|
|
373
|
+
### Utility Functions
|
|
374
|
+
|
|
375
|
+
- **`evaluateConditional(condition, getValue)`**: Evaluate conditional expression
|
|
376
|
+
- **`isFieldVisible(field, getValue)`**: Check field visibility
|
|
377
|
+
- **`isFieldRequired(field, getValue)`**: Check if field is required
|
|
378
|
+
|
|
379
|
+
## Package Structure
|
|
380
|
+
|
|
381
|
+
```
|
|
382
|
+
@formos/kernel/
|
|
383
|
+
├── engine/
|
|
384
|
+
│ ├── factory.ts # createFormEngine, FormEngine interface
|
|
385
|
+
│ ├── values.ts # getValue, setValue operations
|
|
386
|
+
│ ├── validation-ops.ts # validate, getError, isValid
|
|
387
|
+
│ ├── lifecycle.ts # submit, reset
|
|
388
|
+
│ ├── init.ts # Initialization helpers
|
|
389
|
+
│ └── helpers.ts # Internal utilities
|
|
390
|
+
├── state.ts # FormState class
|
|
391
|
+
├── validation.ts # Validation orchestration
|
|
392
|
+
├── effects.ts # Effect execution
|
|
393
|
+
├── conditionals.ts # Conditional evaluation
|
|
394
|
+
├── steps.ts # Multi-step controller
|
|
395
|
+
├── types.ts # Adapter type definitions
|
|
396
|
+
└── index.ts # Public API exports
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
## TypeScript Configuration
|
|
400
|
+
|
|
401
|
+
This package uses **strict mode** TypeScript:
|
|
402
|
+
- `noUncheckedIndexedAccess: true`
|
|
403
|
+
- Optional chaining (`?.`) used everywhere for safety
|
|
404
|
+
- `moduleResolution: "NodeNext"`
|
|
405
|
+
- `module: "NodeNext"`
|
|
406
|
+
|
|
407
|
+
All exports are ESM only (`type: "module"`).
|
|
408
|
+
|
|
409
|
+
## Design Principles
|
|
410
|
+
|
|
411
|
+
### 1. Adapter Pattern
|
|
412
|
+
The kernel doesn't include validation libraries or HTTP clients. Instead, it defines **interfaces** (`ValidatorExecutor`, `EffectExecutor`) that you implement with your preferred tools.
|
|
413
|
+
|
|
414
|
+
### 2. Immutable Schema
|
|
415
|
+
The kernel **never modifies** the schema. It reads the normalized schema and executes its instructions, but the schema remains a pure data structure.
|
|
416
|
+
|
|
417
|
+
### 3. Framework Agnostic
|
|
418
|
+
No React hooks, no DOM manipulation, no browser APIs. This enables:
|
|
419
|
+
- React bindings via `@formos/react`
|
|
420
|
+
- Vue bindings (future)
|
|
421
|
+
- Svelte bindings (future)
|
|
422
|
+
- Node.js usage (server-side form validation)
|
|
423
|
+
|
|
424
|
+
### 4. Optional Chaining Everywhere
|
|
425
|
+
Since schemas are user-provided, the kernel uses optional chaining extensively for safety.
|
|
426
|
+
|
|
427
|
+
## Contributing
|
|
428
|
+
|
|
429
|
+
This package is part of the Formos monorepo. See the root README for contribution guidelines.
|
|
430
|
+
|
|
431
|
+
## License
|
|
432
|
+
|
|
433
|
+
MIT
|