Raw project state management for Formspec. Command dispatch, handler pipeline, undo/redo, cross-artifact normalization, and diagnostics.
This package owns the RawProject class (implementing IProjectCore) and the full handler table — 130 commands across definition, component, theme, mapping, pages, and project areas. It is the foundation that formspec-studio-core composes over.
It manages four editable artifacts:
definition — form structure and behavior (items, binds, shapes, variables)component — UI tree and widget configurationtheme — presentation tokens and cascade rulesmapping — bidirectional data transformsnpm install formspec-core
Runtime dependencies: formspec-engine, formspec-types, ajv
import { createRawProject } from 'formspec-core';
const project = createRawProject();
project.dispatch({
type: 'definition.setFormTitle',
payload: { title: 'Eligibility Intake' },
});
project.batch([
{
type: 'definition.addItem',
payload: { type: 'field', key: 'fullName', label: 'Full name', dataType: 'string' },
},
{
type: 'definition.setBind',
payload: { path: 'fullName', properties: { required: true } },
},
]);
console.log(project.fieldPaths()); // ['fullName']
console.log(project.bindFor('fullName'));
const bundle = project.export();
RawProject is a thin orchestration facade (~380 lines) that delegates to focused subsystems:
RawProject (facade)
├── CommandPipeline — phase-aware dispatch with middleware
├── HistoryManager — undo/redo stacks and command log
├── handlers/ — 17 handler modules aggregated into builtinHandlers
├── queries/ — 7 pure-function query modules
├── tree-reconciler — definition → component tree reconciliation
└── state-normalizer — cross-artifact invariant enforcement
| Module | Responsibility |
|---|---|
pipeline.ts |
CommandPipeline: clones state, runs commands across phases, calls reconcile between phases when signaled, wraps execution with middleware |
history.ts |
HistoryManager<T>: generic undo/redo stacks with depth cap, command log, push/pop/clear operations |
tree-reconciler.ts |
reconcileComponentTree(): pure function — takes definition + existing tree + theme, returns a new component tree preserving bound node properties |
state-normalizer.ts |
normalizeState(): pure function enforcing cross-artifact invariants (URL sync, breakpoint sort, breakpoint inheritance) |
normalization.ts |
normalizeDefinition(): converts legacy serialization shapes to canonical forms (instances array → object, binds object → array); idempotent |
theme-cascade.ts |
resolveThemeCascade(): resolves effective presentation properties for an item across defaults → selectors → item-overrides |
page-resolution.ts |
resolvePageStructure(): resolves theme.pages into enriched ResolvedPageStructure with region existence checks and diagnostics |
component-documents.ts |
Helpers for splitting authored vs. generated component state, creating artifact envelopes |
handlers/index.ts |
Aggregates 17 handler modules into a frozen builtinHandlers table. Each module exports a Record<string, CommandHandler> |
queries/*.ts |
7 modules of pure query functions: (ProjectState, ...) => result. Field queries, expression index, dependency graph, statistics, diagnostics, versioning, registry queries |
RawProject instance receives its own frozen handler table at construction. Custom handlers can be injected via options.handlers.ProjectState is fully JSON-serializable — no Maps or class instances. JSON.stringify(project.state) works.RawProject.dispatch(), batch(), and batchWithRebuild() all delegate to a single _execute(phases) method backed by CommandPipeline. Middleware wraps all paths uniformly.IProjectCore is the seam between this package and formspec-studio-core. It defines the full public API surface.
Command dispatch:
dispatch(command), batch(commands), batchWithRebuild(phase1, phase2)
State getters:
state, definition, component, theme, mapping, artifactComponent, generatedComponent
History:
undo(), redo(), canUndo, canRedo, log, resetHistory()
Queries:
fieldPaths(), itemAt(path), searchItems(filter), statistics(), bindFor(path), componentFor(key), effectivePresentation(key), unboundItems(), resolveToken(key), resolveExtension(name), allDataTypes(), instanceNames(), variableNames(), optionSetUsage(name), listRegistries(), browseExtensions(filter?), responseSchemaRows()
FEL & expressions:
parseFEL(expression, context?), felFunctionCatalog(), availableReferences(context?), allExpressions(), expressionDependencies(expression), fieldDependents(fieldPath), variableDependents(variableName), dependencyGraph()
Diagnostics & versioning:
diagnose(), diffFromBaseline(fromVersion?), previewChangelog(), export()
Every dispatch() follows the same pipeline via _execute(phases):
CommandPipeline.execute():
normalizeState() — cross-artifact invariantsHistoryManager.push() — snapshot for undobatch() groups commands into one phase. batchWithRebuild() uses two phases with inter-phase tree reconciliation.
130 commands across 17 handler modules:
| Area | Commands | Description |
|---|---|---|
definition.* |
46 | Items, binds, shapes, variables, option sets, instances, pages, screener, migrations, metadata |
component.* |
25 | Component tree structure, node properties, custom components, responsive overrides |
theme.* |
28 | Tokens, defaults, selectors, item overrides, pages, grid regions, breakpoints, stylesheets |
mapping.* |
16 | Rules, inner rules, adapter config, preview, extensions |
pages.* |
10 | Page add/delete/reorder, mode, item assignment, region management |
project.* |
5 | Import, subform import, registry loading, publishing |
Custom handlers can be injected at construction:
const project = createRawProject({
handlers: {
'custom.myCommand': (state, payload) => {
// mutate state clone
return { rebuildComponentTree: false };
},
},
});
Custom handlers merge with builtins. Keys override builtins.
Middleware wraps the full dispatch pipeline:
const project = createRawProject({
middleware: [
(state, commands, next) => {
console.log('before:', commands);
const result = next(commands);
console.log('after');
return result;
},
],
});
| Option | Type | Description |
|---|---|---|
seed |
Partial<ProjectState> |
Initial state. Omitted fields get defaults (empty definition, blank artifacts). |
maxHistoryDepth |
number |
Maximum undo snapshots (default: 50). |
middleware |
Middleware[] |
Pipeline wrappers applied to every dispatch. |
handlers |
Record<string, CommandHandler> |
Custom handlers merged over builtins. |
schemaValidator |
SchemaValidator |
When set, diagnose() runs structural validation. Omit in environments without bundled schemas. |
These functions are exported for use outside RawProject:
| Export | Description |
|---|---|
normalizeDefinition(def) |
Converts legacy instances[] → object and binds{} → array. Idempotent. |
resolveThemeCascade(theme, key, type, dataType?) |
Resolves effective presentation properties for one item via the three-level cascade. |
resolvePageStructure(state, itemKeys) |
Resolves page structure from theme.pages with region existence checks. |
resolveItemLocation(state, path) |
Resolves a dot-path to the item and its parent in the definition tree. |
npm run build # tsc
npm run test # vitest run (481 tests)
npm run test:watch # vitest