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

@ez-kit/form-core

v0.3.0

Published

Framework-agnostic form core for @ez-kit/form — field types, select options and error formatting (built on TanStack Form).

Readme

@ez-kit/form-core

Framework-agnostic core for the @ez-kit/form-* packages, built on TanStack Form.

It holds what neither React nor a specific UI kit owns: the closed set of built-in field kinds, the shared SelectOption shape, the normalisation of raw validator errors into display strings — and the whole FormSchema format, the plain-data description of a form that <FormRenderer> walks.

Install

pnpm add @ez-kit/form-core

Most consumers do not install this directly — they use a UI-kit package (@ez-kit/form-shadcn, @ez-kit/form-heroui), which depends on it transitively.

Field kinds and error normalisation

import { FormFieldType, TextInputType, formatFieldErrors, hasFieldErrors } from '@ez-kit/form-core'
import type { LocalizedSelectOption, SelectOption } from '@ez-kit/form-core'
  • FormFieldType / FORM_FIELD_TYPES — the built-in field kinds: text, number, textarea, select, checkbox, switch, radiogroup, slider.
  • TextInputType — the inputType a text field may ask for (email, tel, …).
  • SelectOption<TValue> — { label, value, disabled? } with label already resolved to a string, the option shape every select-like field consumes.
  • LocalizedSelectOption<TValue> — the same shape with label: LocalizedText, which is what a select / radiogroup node in a schema carries, so a delivered document can name a translation key. resolveSelectOptions(options, translate) turns one into the other; FormRenderer calls it for you.
  • formatFieldErrors(errors) — normalises field.state.meta.errors (standard-schema issues, Error instances, plain strings) into string[], dropping empty slots.
  • hasFieldErrors(errors) — the invalid / aria-invalid predicate.

For the hand-written JSX API, validation is pure pass-through of TanStack Form's native standard-schema validators (zod / valibot / arktype). There is no custom resolver.

The FormSchema format

A FormSchema is { version: 1, children: FormNode[] } — a form written as data. It is JSON, with no functions anywhere, so the same document can be authored in TypeScript, stored in a database, or delivered by a backend. <FormRenderer> (from a kit package) renders it through the same bound field components the JSX API uses.

The full guide lives at ez-kit.dev/docs/form/schema.

Authoring

import { defineFormSchema, FormFieldType } from '@ez-kit/form-core'

const schema = defineFormSchema<Values>()({
	version: 1,
	children: [
		{ type: FormFieldType.Text, name: 'email', label: 'Email', validate: { required: true, format: 'email' } },
		{ type: 'submit', label: 'Save' },
	],
})
  • defineFormSchema<TValues, TCustom>()(schema) — curried, because TypeScript has no partial generic inference: TValues is given explicitly while the literal is still inferred, which is what makes name checkable per field kind. TCustom is the closed set of custom field-type keys the document may use.
  • Types: FormSchema, AnyFormSchema, FormNode, FieldNode, SectionNode, StepNode, SubmitNode, BlockNode, CustomFieldNode.
  • GRID_MIN / GRID_MAX / clampToGridRange(n) — the 1..4 range a section's columns and a node's colSpan share.
  • RESERVED_NODE_TYPES — section, step, submit, block; never usable as a registry key.

Parsing an untrusted document

import { FormSchemaError, parseFormSchema } from '@ez-kit/form-core'

const schema = parseFormSchema<Values>(await res.json(), {
	fieldTypes: Object.keys(fields),
	blocks: Object.keys(blocks),
	rules: Object.keys(rules),
	hasTranslate: true,
})

parseFormSchema(input, options?) is the trust boundary for a document that did not come from your own bundle. It throws FormSchemaError (carrying path, the offending node's location) on the first violation — unknown node type, duplicate field name, unregistered rule or block, malformed condition, a columns/colSpan outside 1..4, step mixed with non-step siblings, a translation key with no translate available. Passing the registries' keys via ParseOptions is what lets it check the document against this app's capabilities. Nothing calls it for you.

Conditions

import { compileCondition } from '@ez-kit/form-core'
  • Rule — the closed operator set: { field, eq }, { field, in }, { field, gt }, { field, lt }, { field, truthy: true }, { and }, { or }, { not }. There is no JavaScript in the document and no eval.
  • Condition<TValues> — a Rule, or (TypeScript-authored schemas only) a plain (values) => boolean. parseFormSchema rejects the function form.
  • compileCondition(condition) — turns either into a predicate. A ./-prefixed field reference is reserved for array items and throws in v1.
  • collectRuleFields(condition) / getValueAtPath(values, path) — the two helpers the renderer subscribes and reads with.

Validation from the schema

import { buildValidator } from '@ez-kit/form-core'

buildValidator(schema, { rules, translate }) walks the tree once and returns a single standard-schema validator for the whole form — no zod, no extra runtime dependency — ready for validators.onChange / validators.onSubmit. FieldValidate is the per-field constraint block (required, min/max, minLength/maxLength, format, rule, messages); constraints run in that order and stop at the first failure. There is deliberately no pattern: a regex from an untrusted document is a ReDoS hazard. NamedRule is the (value, values) => true | string implementation a validate.rule key resolves to; an unregistered key throws when the validator is built.

Visibility

import { stripHiddenValues, visibleFieldNames } from '@ez-kit/form-core'
  • visibleFieldNames(schema, values) — the names whose when conditions currently hold, all the way up the tree.
  • stripHiddenValues(schema, values) — the submitted payload with hidden fields removed. An uncontrolled <FormRenderer> applies this for you unless you pass keepHiddenValues; a controlled caller owns onSubmit and calls it themselves. v1 strips top-level keys only.

Traversal and text

  • walkNodes(schema, visitor) / isFieldNode(node) — depth-first traversal, with the ancestor chain.
  • LocalizedText — a finished string, or { key, params }. Translate is (key, params) => string. resolveText(text, translate) resolves one.

License

MIT