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

@maroonedog/waypoint

v0.1.0

Published

A field's address exists before the component that draws it: typed paths the compiler has already checked and runtime state already written, with a contract any validator can describe itself through, resolvers and React and Vue bindings behind one install

Readme

@maroonedog/waypoint

A field's address exists before the component that draws it — as a type the compiler has already checked, and as runtime state that is already written.

npm install @maroonedog/waypoint

ESM only. Node 20+. TypeScript 5.9+ is the tested compiler baseline. Validator and framework packages are optional peers: install the framework whose entry you import, install zod if you import /resolver-zod, and install none of them to use the runtime on its own. The Luq resolver requires @maroonedog/luq@^2.9.0, including when partial execution is disabled.

React quick start

npm install @maroonedog/waypoint react react-dom zod
import { z } from "zod";
import { zodFormResolver } from "@maroonedog/waypoint/resolver-zod";
import { FormProvider, useCreateForm, useField } from "@maroonedog/waypoint/react";

const schema = z.object({ email: z.email() });
const adapter = zodFormResolver(schema);
declare module "@maroonedog/waypoint" {
  interface WaypointForms { contact: typeof adapter }
}

function Email() {
  const field = useField("contact:email");
  return <>
    <label {...field.labelProps}>Email</label>
    <input {...field.inputProps} />
    <span {...field.errorProps}>{field.issues[0]?.message}</span>
  </>;
}

export function App() {
  const form = useCreateForm(() => ({
    key: "contact", adapter, defaultValues: { email: "" },
  }));
  return <FormProvider form={form}>
    <form noValidate onSubmit={event => {
      event.preventDefault();
      void form.submit(values => { console.log(schema.parse(values)); });
    }}>
      <Email />
      <button type="submit">Save</button>
    </form>
  </FormProvider>;
}

Valibot

Install valibot and @valibot/to-json-schema when importing /resolver-valibot.

import * as v from "valibot";
import { valibotFormResolver } from "@maroonedog/waypoint/resolver-valibot";

const adapter = valibotFormResolver(v.object({
  email: v.pipe(v.string(), v.email()),
}));

Descriptors use the official JSON Schema converter. Valibot issue types become typed issue.code values. Forms retain input values; parse in your submit handler if you need transformed output. Async schemas require { fields }, which can be taken from valibotFormResolver(syncShape).fields. Explicit fields also cover types such as Date that JSON Schema cannot describe. All issues are collected, regardless of global abort settings. Optional validation options allow message and language configuration; abort flags cannot be overridden.

zod 4.2.0 is the floor, and the reason is measured. The resolver reads ~standard.jsonSchema, the Standard Schema JSON Schema companion, and zod ships it from 4.2.0: on 4.1.12 that property is undefined, on 4.2.0 it is an object. Under a zod that does not have it, zodFormResolver describes no fields at all — the form renders inputs with no type, no bounds and no aria, and the only signal is one console line. The peer range used to admit that.

The one file nobody guesses

Paths are typed by a module augmentation, not by a prop. Write this once, in your own source, and every hook in the application is checked — including components that import nothing from it.

// src/waypoint-forms.ts
import { zodFormResolver } from "@maroonedog/waypoint/resolver-zod";
import { orderSchema } from "./order-schema.js";

export const orderAdapter = zodFormResolver(orderSchema);

declare module "@maroonedog/waypoint" {
  interface WaypointForms {
    form: typeof orderAdapter;
  }
}

useField("billing.postcode") now infers its value type, and useField("billing.postcod") is a compile error. An application that registers nothing is refused rather than quietly unchecked.

Entry points

Reusable packages should export adapters and shared controls without augmenting WaypointForms from their ordinary entry. Register forms in application source and use qualified paths when sharing components. An incompatible duplicate key is rejected when declarations are checked, but skipLibCheck can suppress conflicts inside dependency declarations. Global registration is not isolation.

The entry points install together — what the split buys is not bytes but resolution. ./core loads in a worker where React cannot be resolved at all, and ./react is the only entry whose built output names React.

| Entry | What you import from it | |---|---| | @maroonedog/waypoint | The contract, the registry and the path types: FormAdapter, FormIssue, WaypointForms, FormPath, FormPathTo, FieldPath, ConcretePath. declare module goes here. One runtime export, isPending. | | @maroonedog/waypoint/core | The runtime: createForm, createCellStore, assertFormStoreContract. No React, no validator, no DOM. | | @maroonedog/waypoint/react | useField, useRows, useFormStatus, FormProvider, <Field>, <AutoForm>. | | @maroonedog/waypoint/vue | useField, useRows, useFormStatus, FormProvider, <Field>, <FieldRows> for Vue 3, against the SAME registry — the declare module names the package, not a binding. It carries no <AutoForm>, no uncontrolled binding and no error summary yet; everything they are built on is reachable through useForm(). | | @maroonedog/waypoint/resolver-standard | standardFormResolver — any validator that implements Standard Schema and its JSON Schema companion. Names no vendor. | | @maroonedog/waypoint/resolver-zod | zodFormResolver: the above, plus the three facts zod's JSON Schema does not carry about zod. zod is a type-only import, erased at build time. | | @maroonedog/waypoint/resolver-luq | luqFormResolver: the above, plus luq's issue codes and severities, which the spec has no member for. | | @maroonedog/waypoint/resolver-valibot | valibotFormResolver: Valibot descriptors, native issue codes and optional partial execution. Requires valibot and @valibot/to-json-schema. | | @maroonedog/waypoint/store-zustand | createZustandCellStore — a shipped instance of the store contract ./core exports. |

Source ships in the tarball alongside declaration maps, so Go-to-Definition lands on the file that explains why the code is the way it is rather than on a signature.

What it does not do

This list is here because a capability list without one is an advertisement. Each line says what it cannot do and what to reach for instead.

  • Partial execution is opt-in. Use zodFormResolver(schema, { partial: true }) to validate selected top-level subtrees on edits and field validation. Root refinements fall back to full validation; submit always validates everything. form.validate(["email"]) requests scopes explicitly. Custom adapters can implement validatePartial with complete replacement scopes and dependency expansion. Valibot also supports { partial: true } for object schemas; root pipes and unsupported shapes use full validation. With Luq 2.9+, use luqFormResolver(schema, { partial: true, dependencies: { password: ["confirmation"] } }); declare cross-subtree dependencies explicitly. Standard Schema remains whole-form.
  • No debounceMs. A pass can include synchronous and async rules, so a delay on the pass would delay the required-field message along with the network rule. Debounce inside your async rule; the optional second argument to validate is an AbortSignal, aborted the moment a newer pass starts, so a superseded round trip can stop rather than finish and be discarded.
  • No per-field validateOn. createForm takes "change" | "blur" | "submit" for the whole form. A partial pass can include dependent fields, so the trigger and the visibility of each field's errors are separate settings.
  • No multi-file input, and no file byte or MIME constraint. kind: "file" is one File: a multi-file pick replaces a whole list in one event, and rows() has insert, remove and move with no replace-the-list edit. Byte bounds are dropped rather than carried under minLength, which already means characters and array elements. Draw the multiple input and the accept attribute yourself, and keep the list with rows().
  • No FormData decoder, no server actions, no RSC data flow. ./react carries "use client", so it imports into a Server Component without a wrapper; and every input already carries name set to its concrete path, so a posted form arrives as flat pairs keyed owner.name and items[0].quantity. Nothing here folds them back into a root, because an unchecked checkbox sends no entry at all and an untouched optional field sends "" — and both need a policy this package will not guess. Decode it yourself, or post JSON from form.readRoot().
  • No React Native bindings. ./core has no DOM and is portable; the bindings are not — inputProps emits minLength, pattern and aria-describedby, and useUncontrolledField writes an HTMLInputElement. Use ./core and write the bindings.
  • No devtools extension. There is the seam one would be built on: createForm takes a store, a store is five members over an opaque key, so a store that records every write is an ordinary substitution rather than an instrumentation hook. The documentation site runs one under a form you can type into.

The repository's README carries the rest, including the limits of the path grammar and of describing a union branch.

Where it stands

This is the initial 0.1.0 release line. Production adoption is not established; APIs may change during 0.x. Registration is global to a TypeScript program, not isolated per package or Provider. Multiple installed waypoint versions and declaration-bundler rewrites have not been validated. Keys containing dots are not supported by the path grammar. Source and declaration maps ship in the package; the repository and documentation site may not be publicly accessible.

MIT.