npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@formspec-org/engine

v1.0.0

Published

Client-side form state engine with FEL expression language, reactive signals, and validation

Downloads

159

Readme

formspec-engine

Core form state engine for Formspec. Manages field values, relevance, required state, readonly state, validation results, and repeat group counts via a reactive signal graph.

Where spec logic runs: Normative FEL (parse, dependency lists, analysis, prepare, eval), validation, coercion, migrations, batch definition eval, etc. is implemented in Rust and exposed through WASM (wasm-pkg-runtime / wasm-pkg-tools). TypeScript is orchestration: Preact signals, FormEngine, and thin src/fel/ modules (fel-api-runtime.ts, fel-api-tools.ts) that call the bridges — not a second in-tree FEL parser. See CLAUDE.md / AGENTS.mdArchitectureLogic ownership (Rust / WASM first).

Runtime dependencies: @preact/signals-core ^1.6.0, formspec-types (workspace) Module format: ESM (dist/index.js) Build: npm run build (two wasm-pack outputs under wasm-pkg-runtime/ / wasm-pkg-tools/, then tscdist/)


Install

This package lives in the monorepo. Reference it from a sibling package:

"dependencies": {
  "formspec-engine": "*"
}

Build before use:

npm run build

Quick Usage

import { FormEngine } from 'formspec-engine';

const engine = new FormEngine({
  url: 'my-form',
  version: '1.0',
  items: [
    { key: 'name',  type: 'field', dataType: 'string', label: 'Name' },
    { key: 'age',   type: 'field', dataType: 'integer', label: 'Age' },
    { key: 'total', type: 'field', dataType: 'decimal', label: 'Total',
      calculate: '$price * $qty' }
  ]
});

// Write values
engine.setValue('name', 'Alice');
engine.setValue('age', 30);

// Read current value
console.log(engine.signals['name'].value);   // 'Alice'

// Check validation
const report = engine.getValidationReport({ mode: 'submit' });
console.log(report.valid, report.counts);

// Collect response
const response = engine.getResponse();
console.log(response.data);

API Surface

FormEngine

new FormEngine(
  definition: FormspecDefinition,
  runtimeContext?: FormEngineRuntimeContext
)

Reactive Signal Properties

All signals are @preact/signals-core primitives. Read .value directly, or read inside computed() / effect() to subscribe reactively.

| Property | Type | Description | |---|---|---| | signals | Record<string, Signal<any>> | Field values. Keys are dotted paths with 0-based brackets (group[0].field). Writable signals for plain fields; read-only computed signals for calculate binds. | | relevantSignals | Record<string, Signal<boolean>> | Visibility per path. true by default; computed when a relevant FEL expression is set. | | requiredSignals | Record<string, Signal<boolean>> | Required state per path. | | readonlySignals | Record<string, Signal<boolean>> | Readonly state per path. | | errorSignals | Record<string, Signal<string\|null>> | First error message (or null) per field. Derived from validationResults. | | validationResults | Record<string, Signal<ValidationResult[]>> | Full bind-level results per path. | | shapeResults | Record<string, Signal<ValidationResult[]>> | Results per shape ID for continuous-timing shapes. | | repeats | Record<string, Signal<number>> | Instance count per repeatable group path. | | optionSignals | Record<string, Signal<FormspecOption[]>> | Options per field (inline, optionSets, or remote). | | optionStateSignals | Record<string, Signal<RemoteOptionsState>> | { loading, error } for remote options. | | variableSignals | Record<string, Signal<any>> | Computed variables keyed as "scope:name" (e.g. "#:globalRate"). | | dependencies | Record<string, string[]> | Dependency graph: path → paths it reads. | | structureVersion | Signal<number> | Increments on structural changes (add/remove repeat). FEL closures read this to re-evaluate after structure changes. |

Methods

Value management

setValue(path: string, value: any): void
// Normalizes whitespace (trim/normalize/remove), coerces strings to numbers for
// numeric dataTypes, and applies precision rounding — per bind config.

Response and validation

getResponse(meta?: { id?, author?, subject?, mode? }): object
// Returns { definitionUrl, definitionVersion, status, data, validationResults, authored }.
// status: 'completed' if valid, 'in-progress' otherwise.
// Non-relevant fields handled per nonRelevantBehavior: remove (default) | empty | keep.

getValidationReport(options?: { mode?: 'continuous' | 'submit' }): ValidationReport
// Collects bind-level results (filtered by relevance), continuous shape results,
// and — if mode='submit' — evaluates submit-timing shapes.
// valid = true iff counts.error === 0.

evaluateShape(shapeId: string): ValidationResult[]
// Evaluates a single shape by ID (for demand-timing shapes).

Repeat groups

addRepeatInstance(itemName: string): number | undefined
// Returns the new 0-based index. Initializes all child signals.

removeRepeatInstance(itemName: string, index: number): void
// Snapshots values, splices the index, rebuilds signals, restores values.

FEL compilation

compileExpression(expression: string, currentItemName?: string): () => any
// Returns a reactive closure. Call inside computed() to auto-subscribe
// to all referenced field signals.

Variables

getVariableValue(name: string, scopePath: string): any
// Walks from scopePath upward to global scope ('#'), returns first match.

Screener

evaluateScreener(): { target: string; label?: string } | null
// Evaluates definition.screener.routes in order.
// Returns the first route with a truthy condition, or null.

Diagnostics and replay

getDiagnosticsSnapshot(options?: { mode? }): FormEngineDiagnosticsSnapshot
// Full snapshot: all values, MIP states, dependencies, validation, runtime context.

applyReplayEvent(event: EngineReplayEvent): EngineReplayApplyResult
replay(events: EngineReplayEvent[], options?: { stopOnError? }): EngineReplayResult

Runtime context

setRuntimeContext(context: FormEngineRuntimeContext): void
// context: { now?, locale?, timeZone?, seed? }

Migration

migrateResponse(responseData: Record<string, any>, fromVersion: string): Record<string, any>
// Applies definition.migrations filtered by fromVersion, sorted ascending.
// Change types: rename, remove, add, transform (FEL expression).

i18n

setLabelContext(context: string | null): void   // e.g. 'es', 'fr'
getLabel(item: FormspecItem): string             // Returns locale label or item.label

Other Exports

Definition assembly — resolves $ref inclusions into a self-contained definition:

import { assembleDefinition, assembleDefinitionSync } from 'formspec-engine';

const result = await assembleDefinition(definition, resolver);
// result: { definition: FormspecDefinition, assembledFrom: AssemblyProvenance[] }

The assembler prefixes keys, rewrites bind paths, rewrites shape targets, rewrites FEL expressions, imports variables, detects key/variable/shape-ID collisions, and records provenance.

FEL analysis — static analysis without a running engine:

import { analyzeFEL, getFELDependencies, rewriteFELReferences } from 'formspec-engine';

Extension validation — checks extensions fields against loaded registry entries:

import { validateExtensionUsage } from 'formspec-engine';

Runtime mapping — bidirectional data mapping independent of FormEngine:

import { RuntimeMappingEngine } from 'formspec-engine';

const mapper = new RuntimeMappingEngine(mappingDocument);
const forward = mapper.forward(source);
const reverse = mapper.reverse(source);

Schema validation — validates Formspec documents against JSON schemas:

import { createSchemaValidator } from 'formspec-engine';

Path utilities:

import { itemAtPath, normalizeIndexedPath, splitNormalizedPath } from 'formspec-engine';

FEL function catalog — for editor tooling and docs generation:

import { getBuiltinFELFunctionCatalog } from 'formspec-engine';

Key Types

interface FormspecDefinition {
  url: string;
  version: string;
  title?: string;
  items: FormspecItem[];
  binds?: FormspecBind[];
  shapes?: FormspecShape[];
  variables?: FormspecVariable[];
  instances?: FormspecInstance[];
  optionSets?: Record<string, FormspecOption[]>;
  migrations?: Migration[];
  screener?: Screener;
  formPresentation?: any;
}

interface FormspecItem {
  key: string;
  type: 'field' | 'group' | 'section' | string;
  dataType?: string;
  label?: string;
  options?: FormspecOption[];
  optionSet?: string;
  repeatable?: boolean;
  minRepeat?: number;
  maxRepeat?: number;
  pattern?: string;
  // Inline bind shorthand (merged with definition.binds):
  relevant?: string;
  required?: string | boolean;
  calculate?: string;
  readonly?: string | boolean;
  constraint?: string;
  constraintMessage?: string;
  default?: any;
  nonRelevantBehavior?: 'remove' | 'empty' | 'keep';
}

interface FormspecBind {
  path: string;           // supports [*] wildcards
  relevant?: string;
  required?: string | boolean;
  calculate?: string;
  readonly?: string | boolean;
  constraint?: string;
  constraintMessage?: string;
  default?: any;
  nonRelevantBehavior?: 'remove' | 'empty' | 'keep';
  remoteOptions?: string;
  whitespace?: 'trim' | 'normalize' | 'remove';
  precision?: number;
}

interface ValidationReport {
  valid: boolean;
  results: ValidationResult[];
  counts: { error: number; warning: number; info: number };
  timestamp: string;  // ISO 8601
}

interface ValidationResult {
  path: string;              // 1-based external path
  message: string;
  severity: 'error' | 'warning' | 'info';
  constraintKind: 'type' | 'required' | 'constraint' | 'minRepeat' | 'maxRepeat';
  code: string;              // TYPE_MISMATCH | REQUIRED | CONSTRAINT_FAILED | PATTERN_MISMATCH | MIN_REPEAT | MAX_REPEAT
  context?: Record<string, any>;
  constraintMessage?: string;
}

interface FormEngineRuntimeContext {
  now?: Date | string | number | (() => Date | string | number);
  locale?: string;
  timeZone?: string;
  seed?: string | number;
}

Architecture

Signal graph

The engine builds a reactive signal graph on construction. Three @preact/signals-core primitives:

  • signal(value) — writable. Used for: plain field values, static MIP states, repeat counts, option lists, structureVersion.
  • computed(fn) — read-only derived. Used for: calculate field values, FEL-based MIP states, validation results, error signals, variable signals.
  • effect(fn) — side effect. Used for: applying bind.default values on relevance transitions.

Reactive FEL uses compileExpression() closures: each closure reads structureVersion, instance/evaluation version signals, and calls wasmEvalFELWithContext with a JSON context built from engine state. Preact captures signal reads when the closure runs inside a computed. Dependency lists for binds (e.g. calculate) come from wasmGetFELDependencies during definition setup — same Rust parser as eval, not a TypeScript CST walk.

FEL surface in this package (src/fel/)

  • fel-api-runtime.ts — WASM runtime only: analyzeFEL, getFELDependencies, evaluateDefinition, path helpers (normalizeIndexedPathwasmNormalizeIndexedPath; splitNormalizedPath defers to WASM then splits). itemLocationAtPath walks the in-memory definition tree by key (host navigation). normalizePathSegment is a small exported string helper; full paths should use normalizeIndexedPath / splitNormalizedPath for Rust-aligned behavior.
  • fel-api-tools.ts — lazy tools WASM: tokenize/print/catalog, rewrites, lint-adjacent FEL helpers, etc.
  • fel-api.ts — re-exports both for import 'formspec-engine'.

Grammar, stdlib, and evaluation semantics live in fel-core / formspec-core (Rust); see crates/fel-core and crates/formspec-wasm.

Standard library (44+ functions)

| Category | Functions | |---|---| | Aggregates | sum, count, avg, min, max, countWhere | | String | upper, lower, trim, length, contains, startsWith, endsWith, substring, replace, matches, format | | Math | abs, power, round, floor, ceil | | Date/time | today, now, year, month, day, hours, minutes, seconds, dateAdd, dateDiff, time, timeDiff | | Logical | coalesce, isNull, present, empty, if | | Type check | isNumber, isString, isDate, typeOf | | Cast | string, number, boolean, date | | Choice | selected | | Money | money, moneyAmount, moneyCurrency, moneyAdd, moneySum | | Navigation | prev, next, parent | | MIP query | valid, relevant, readonly, required | | Instance | instance |

Validation

Bind-level — each field's validationResults signal evaluates in order: type check → required → constraint expression → pattern. Cardinality checks on repeatable groups produce MIN_REPEAT / MAX_REPEAT results.

Shape rules — cross-field constraints in definition.shapes. Each shape has a target path, severity, timing (continuous | submit | demand), optional activeWhen guard, and a composition operator: constraint, and, or, not, or xone. Continuous shapes run as computed signals; submit shapes run at report time; demand shapes run via evaluateShape(id).

Path resolution

  • Simple: fieldName
  • Dotted: group.child.field
  • Indexed (internal): group[0].field (0-based)
  • Indexed (external, in ValidationResult): group[1].field (1-based)
  • Wildcard (binds/shapes): items[*].field — expanded via resolveWildcardPath using current repeat counts

Definition assembly

assembleDefinition resolves $ref group items into a self-contained definition. For each $ref the assembler: fetches the referenced definition, selects the fragment, applies keyPrefix, rewrites bind paths and shape targets into the host scope, rewrites all $-prefixed FEL references, imports variables, detects collisions, records provenance, and recurses into nested $ref items.

Rust / WASM (split artifacts)

npm run build compiles crates/formspec-wasm twice via wasm-pack and runs the same wasm-opt pass as before. The runtime build passes --no-default-features, which disables the full-wasm meta-feature: no formspec-lint, and no optional wasm_bindgen modules (document/plan, assembly, mapping, registry, changelog, FEL authoring helpers). Those exports exist only in the tools artifact. See crates/formspec-wasm README → Cargo features.

| Output directory | Glue module prefix | Used for | |------------------|-------------------|----------| | wasm-pkg-runtime/ | formspec_wasm_runtime* | Default initFormspecEngine() path: FormEngine, batch eval, FEL eval, coercion, migrations, option-set inlining, path helpers | | wasm-pkg-tools/ | formspec_wasm_tools* | Lint (7-pass) + schema planning, registry document helpers, mapping execution, definition assembly in WASM, FEL authoring helpers (tokenize, print, rewrites, …) |

  • Call await initFormspecEngine() before FormEngine or runtime WASM helpers.
  • Call await initFormspecEngineTools() before sync tooling APIs (lintDocument, tokenizeFEL, assembleDefinitionSync, RuntimeMappingEngine, …). await assembleDefinition() loads tools lazily on first use.
  • Paired artifacts expose formspecWasmSplitAbiVersion(); the JS bridge rejects mismatched runtime/tools builds.

Runtime-only startup (smaller static graph): init-formspec-engine.ts imports only wasm-bridge-runtime and uses import('./wasm-bridge-tools.js') when tools init runs. The FormEngine implementation imports runtime bridge only (not the compatibility barrel). The package root (import 'formspec-engine') still re-exports fel-api, which composes fel-runtime + fel-tools (both bridges), so a full index load still parses tools glue. Use formspec-engine/fel-runtime (and /fel-tools only where needed) to avoid that — e.g. formspec-core does. For embedders that only need startup + runtime WASM, import the subpath:

import { initFormspecEngine, isFormspecEngineInitialized } from 'formspec-engine/init-formspec-engine';

Render / <formspec-render> surface: import formspec-engine/render for createFormEngine, FormEngine, IFormEngine, response helpers, and inits — same runtime WASM path as above, without the FEL tooling facade (fel-api) or static tools bridge. formspec-webcomponent uses this subpath.

(package.json exports exposes ./init-formspec-engine, ./render, ./fel-runtime (path + analyzeFEL / evaluateDefinition / runtime WASM only), and ./fel-tools (lint, registry, tokenize, rewrites, etc.). formspec-core imports fel-runtime / fel-tools so handlers that only need path helpers do not pull tools glue through the main package entry.)

Run npm run build in this package (or the monorepo root) to produce wasm-pkg-runtime/ and wasm-pkg-tools/.

Size profiling: After npm run build:wasm, run npm run profile:twiggy for twiggy on both .wasm files (top, --retained, diff runtime→tools, monos, garbage). Complements cargo bloat on formspec-wasm proxy bins (crate names on host vs real wasm mass). Monorepo root: npm run wasm:twiggy. See thoughts/reviews/2026-03-23-wasm-split-baseline.md.

Git / npm publish: these directories are not root-gitignored (so npm pack / npm publish can include them per package.json files). wasm-pack writes a pkg-local .gitignore containing *; the build scripts delete that file so npm does not skip the WASM tree. Do not commit wasm-pkg-runtime/ or wasm-pkg-tools/ — keep them untracked build outputs. prepack runs npm run build before pack.


Tests

Run with Node.js built-in test runner:

npm test          # build + init-entry grep gate + unit tests + runtime/tools isolation checks
npm run test:unit # test only (requires prior build; initializes runtime + tools WASM)
npm run test:init-entry-runtime-only # grep dist/init-formspec-engine.js (no tools wasm path)
npm run test:render-entry-runtime-only # grep dist/engine-render-entry.js (no fel facade / tools bridge)
npm run test:fel-runtime-entry-only # grep dist/fel/fel-api-runtime.js (no tools bridge)
npm run test:wasm-runtime-isolation # runtime-only init (no global setup)

20 test files in tests/ covering: bind behaviors, bind defaults and expression context, definition assembly (sync/async), FEL path rewriting, shape composition and timing, repeat lifecycle, response pruning, remote options, runtime diagnostics, replay, and runtime mapping.