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

@texaryn/core

v0.13.0

Published

Headless runtime for JSON Schema forms and schema-driven UI: a framework-neutral UI IR, compiler, form state and renderer registry, used with @texaryn/schema-json and a React, Vue or Web Components binding

Readme

@texaryn/core

Headless runtime for JSON Schema forms and schema-driven UI: a framework-neutral UI IR, compiler, form state and renderer registry, used with @texaryn/schema-json and a React, Vue or Web Components binding.

Status: pre-1.0. Public APIs may change before 1.0.

Install

pnpm add @texaryn/core

@texaryn/core has no React, Vue or DOM dependency.

What it does

Texaryn keeps schema evaluation, UI structure, runtime state, and rendering separate:

SchemaEvaluationPort
        |
        v
SchemaProjection
        |
        v
     compile()
        |
        v
    UIDocument
        |
        v
  FormRuntime
        |
        v
    Renderer

The core package owns the framework-neutral contracts and deterministic runtime. A schema adapter supplies semantic information through SchemaEvaluationPort, and a renderer consumes the resulting UI document and runtime state.

Quick start

Most applications use @texaryn/core together with a schema adapter and a renderer.

import { createFormRuntime } from '@texaryn/core'
import { createJsonSchemaAdapter } from '@texaryn/schema-json'

const schema = {
  type: 'object',
  properties: {
    name: { type: 'string', title: 'Name' },
  },
  required: ['name'],
}

const adapter = await createJsonSchemaAdapter(schema)

const runtime = createFormRuntime(adapter, {
  initialData: { name: '' },
  onSubmit(data) {
    console.log(data)
  },
})

const document = runtime.document.getSnapshot()
const data = runtime.data.getSnapshot()

console.log(document.rootId, data)

runtime.dispatch({ type: 'Submit' })

runtime.destroy()

Schema evaluation contract

Schema libraries integrate with Texaryn through SchemaEvaluationPort:

interface SchemaEvaluationPort {
  project(data: unknown): SchemaProjection
  validate(data: unknown): ValidationResult | Promise<ValidationResult>
  validateAt?(
    data: unknown,
    pointer: JsonPointer,
  ): ValidationResult | Promise<ValidationResult>
}

The core does not parse or validate JSON Schema itself. That responsibility belongs to schema adapter packages such as @texaryn/schema-json.

Runtime

Create a runtime with:

import { createFormRuntime } from '@texaryn/core'

const runtime = createFormRuntime(port, {
  initialData,
  hints,
  onSubmit,
})

A FormRuntime exposes stores for:

  • document, the current UIDocument
  • data, the current form data
  • submission, the current submission state
  • visibleErrors, every error currently shown across the form

It also exposes:

  • dispatch(command)
  • getNodeState(nodeId)
  • destroy()

Node state is kept outside the UI IR and includes value, validation, dirty/touched state, visibility, and disabled state.

Read-only is not node state: it comes from the schema, so it lives on the compiled node as readOnly and is resolved rather than raw. A read-only object or array makes everything beneath it read-only too, because editing a descendant changes the ancestor's value. The runtime refuses SetValue, InsertItem, RemoveItem and MoveItem on a read-only node, so a renderer cannot write past it. Reset is deliberately exempt: it is the owning authority replacing state wholesale rather than a user edit, and it is how a server-owned value legitimately changes.

Commands

The runtime currently understands:

{ type: 'SetValue', nodeId, value }
{ type: 'InsertItem', containerId, index, value? }
{ type: 'RemoveItem', containerId, index }
{ type: 'MoveItem', containerId, from, to }
{ type: 'SetTouched', nodeId }
{ type: 'Submit' }
{ type: 'Reset', data? }

Commands are data. They do not contain renderer or DOM behavior.

UI IR

compile() converts a SchemaProjection into a flat, versioned UIDocument:

interface UIDocument {
  version: 1
  rootId: NodeId
  nodes: Record<NodeId, UINode>
}

The IR contains semantic nodes such as fields, containers, text, and actions. It describes what the UI means, not how a framework should render it.

UI hints

Presentation can be layered on top of schema semantics:

const hints = {
  '/email': {
    placeholder: '[email protected]',
    order: 1,
  },
  '/bio': {
    widget: 'textarea',
    helpText: 'Tell us about yourself.',
    order: 2,
  },
}

Hints can also control when validation runs:

const hints = {
  '/email': { validationTrigger: 'blur' },
  '/search': { validationTrigger: 'change' },
}

Three triggers are supported: blur validates on field blur (SetTouched), change validates after a debounce on value changes (SetValue, array mutations), and submit validates only on form submission. Fields without a validationTrigger hint are not automatically validated on blur or change; submit always validates the entire form regardless of hints.

The change debounce defaults to 300ms and can be configured:

const runtime = createFormRuntime(port, {
  initialData,
  hints,
  validationDebounceMs: 500,
})

Validation remains full-form: port.validate(data) is called with the entire form data, and errors are distributed to nodes by their data pointers. Synchronous validators skip the pending state entirely.

Errors are not displayed immediately. The runtime computes a showErrors flag per node: a field's errors become visible once it is invalid and either touched (the user has interacted with it) or the form has been submitted, because a failed Submit has to show what blocked it, including fields the user never reached. submission.attempts counts the accepted Submit commands since creation or the last Reset, and the gate stays open while it is above zero. The form-level visibleErrors store aggregates all currently visible errors for use in error summaries. The useField hook in both @texaryn/react and @texaryn/vue returns showErrors, and the React getInputProps sets aria-invalid and the error ID in aria-describedby only when showErrors is true.

Pass hints to createFormRuntime(), or to useForm() in React or Vue.

Submission lifecycle

Dispatching Submit captures the current form data as an immutable snapshot and triggers full-form validation against it. If validation passes, the runtime transitions to submitting and calls onSubmit with the captured data:

const runtime = createFormRuntime(port, {
  initialData: { name: '' },
  onSubmit: async (data) => {
    await api.save(data)
  },
})
runtime.dispatch({ type: 'Submit' })

The submission store tracks the lifecycle: idle, validating, submitting, submitted. It also carries attempts, the number of accepted Submit commands since creation or the last Reset. A failed validation returns to idle with every invalid field showing its errors; a failed onSubmit returns to idle with the error on submission.error. Dispatching Submit while already validating or submitting is a no-op.

Edits during validating cancel the submission attempt and return to idle. Edits during submitting update the live form data but do not alter the in-flight payload or trigger validation. A Reset during submission cleanly cancels via a generation counter, so late completions from abandoned attempts are ignored.

Renderer registry

The core exports a framework-neutral renderer registry:

import { createRendererRegistry } from '@texaryn/core'

const registry = createRendererRegistry()

Framework packages can register concrete widget components without adding framework dependencies to the runtime.

Stable array identity

The core includes identity helpers used to keep dynamic array items stable across inserts, removals, moves, and recompilation. Arrays are addressed by ArrayMeta.identityKey, an opaque key for the logical container that stays the same when rows above it move or change shape, so a nested array keeps its item identities when its parent row moves.

Identity follows structural edits: InsertItem, RemoveItem and MoveItem keep the ids of every item they do not touch, including the nested arrays inside a moved row. Reset is wholesale state replacement rather than a structural edit: it re-establishes identity by matching old and new items (by ArrayMeta.itemKey when set, otherwise by content), so the nested arrays under a row that changed position may be minted afresh.

The helpers:

  • createIdentityMap
  • registerArray
  • insertItem
  • removeItem
  • moveItem
  • resolvePointer
  • reconcile

Key exports

Core types

  • NodeId
  • StableItemId
  • JsonPointer
  • MaybePromise
  • JsonSchemaType
  • ValidationResult
  • ValidationError
  • VisibleError

Schema and IR

  • SchemaEvaluationPort
  • SchemaProjection
  • NodeProjection
  • ChildProjection
  • AnnotationSet
  • UIDocument
  • UINode
  • NodeBase
  • NodeAnnotations
  • FieldNode
  • FieldType
  • FieldConstraints
  • EnumOption
  • ContainerNode
  • ArrayMeta
  • TextNode
  • ActionNode
  • CompileResult
  • compile

UI hints

  • UIHints
  • FieldHints
  • ArrayHints

Messages

  • FormMessages, ActionMessage, IndicatorMessage, ItemActionContext, AddItemContext
  • englishMessages
  • mergeMessages

Runtime and state

  • createFormRuntime
  • FormRuntime
  • FormRuntimeOptions
  • NodeState
  • RuntimeState
  • NodeRuntimeState
  • ValidationState
  • InteractionState
  • SubmissionState
  • IdentityMap
  • createStore
  • Store
  • WritableStore

Commands

  • Command
  • Effect
  • CommandResult
  • processCommand

JSON Pointer helpers

  • getAtPointer
  • setAtPointer
  • parsePointer

setAtPointer states its contract in full on the function itself: own members only, what an absent or null level becomes, which tokens address an array element, and which writes are refused rather than guessed. getAtPointer is total and returns undefined for anything it cannot address.

Stable array identity

  • createIdentityMap
  • registerArray
  • insertItem
  • removeItem
  • moveItem
  • resolvePointer
  • reconcile
  • ReconcileOptions
  • identityKey
  • ROOT_IDENTITY_KEY
  • IdentityKey
  • IdentitySegment

Rendering

  • createRendererRegistry
  • RendererRegistry
  • WidgetTester
  • WidgetEntry

Design constraints

@texaryn/core intentionally does not depend on:

  • React, Vue or any other rendering framework
  • the DOM
  • a JSON Schema implementation
  • an AI SDK or transport protocol

That boundary is what allows the same runtime model to support different schema engines and renderers.

Related packages

See the repository README and ROADMAP for the full architecture.

License

Apache-2.0