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

@rusl-labs/surface

v0.1.1

Published

Validator-agnostic recursive React runtime for JSON Schema forms and displays.

Readme

@rusl-labs/surface

The core runtime. createSurfaceUi returns the Surface component and the kit registry.

You do not need to read this to get started. The root README covers the setup. Read this when you need the API reference or want to understand how Surface resolves schemas and renderers.

createSurfaceUi(options)

function createSurfaceUi(options: {
  schemaResolver: SchemaResolver;
  validator: SurfaceValidator;
  kit: SurfaceKit;
  annotationResolver?: AnnotationResolver;
}): { Surface: React.FC<SurfaceProps> }

| Option | What it does | | --- | --- | | schemaResolver | Loads JSON Schema documents by $id. Surface calls it whenever it encounters a $ref to an unknown document. | | validator | Validates data against the schema before onSubmit. Must implement SurfaceValidator. | | kit | Supplies the renderers. Must implement SurfaceKit. | | annotationResolver | Optional. Loads annotation documents by URI. |

Call createSurfaceUi once at the top of your app. It returns a bound Surface component ready to mount anywhere:

const { Surface } = createSurfaceUi({
  schemaResolver: new InMemorySchemaFetchResolver({ [schema.$id]: schema }),
  validator: createAjvValidator(),
  kit: createHtmlKit(),
});

// Mount it anywhere
<Surface id={schema.$id} data={data} onChange={setData} />

Surface props

| Prop | Type | Required | What it does | | --- | --- | --- | --- | | id | string | Yes | Schema $id to render | | data | unknown | — | Controlled value | | onChange | (next: unknown) => void | — | Called when any field changes. Omit for an internal draft. | | mode | "input" | "display" | — | Default "input" | | view | string | — | Default "default". Open string — use "card", "row", "identity", or your own. | | onSubmit | ({ data }: { data: unknown }) => void | — | Called after a successful validate-and-save | | labels | boolean | — | Inherited. false hides field chrome for this node and descendants. A child can override with true. | | schema / document / documentUri | — | — | Pass the schema document directly when you already have it | | annotationUri | string | — | URI of an annotation document for this mount |

Schema resolver

Surface walks JSON Schema graphs. When it encounters a $ref, it asks the resolver for that document.

InMemorySchemaFetchResolver

Seed it with documents keyed by $id. Unknown $id values are fetched over the network.

const resolver = new InMemorySchemaFetchResolver({
  [PERSON_ID]: personSchema,
  [ADDRESS_ID]: addressSchema,
});

This works for $ref across documents and #/$defs/… fragments inside them.

Custom resolver

Implement SchemaResolver to load schemas from your own store:

interface SchemaResolver {
  resolve(uri: string): Promise<JsonSchemaDocument | undefined>;
}

Kit registry

The kit picks a renderer for each schema node. It receives a RendererRequest with candidate keys and returns a React component — or falls through to the next entry.

createRegistryKit(options)

Build a kit from a list of resolver entries:

import { createRegistryKit } from "@rusl-labs/surface";

const kit = createRegistryKit({
  fallback: () => null,
  resolvers: [
    { key: PERSON_ID, mode: "display", view: "card", component: PersonCard },
  ],
  Root: MyFormShell,
  aliases: { "date-time": "datetime" },
});

createHtmlKit({ resolvers, aliases }) wraps this — your entries append after the HTML kit defaults.

Registry entry shape

type RegistryEntry =
  | { key?: string; mode?: SurfaceMode; view?: SurfaceViewName;
      component: SurfaceRenderer }
  | { key?: string; mode?: SurfaceMode; view?: SurfaceViewName;
      resolve: (request: RendererRequest) => SurfaceRenderer | null };

| Field | What it matches | | --- | --- | | key | A candidate key. Omit for a catch-all that runs before keyed lookup. | | mode | "input" or "display". Omit for both. | | view | View name. Omit for all views. A non-default view falls back to a view: "default" entry. | | component | Always use this renderer. | | resolve | Return a renderer, or null to keep falling through. Use when the choice depends on app state. |

Never set both component and resolve. Last registration wins when multiple entries match the same key.

kit.set() after init

kit.set(PERSON_ID, "display", "card", PersonCard);
kit.set({ key: PERSON_ID, mode: "display", view: "card", component: PersonCard });

kit.Root

When set, the engine wraps only the root body's renderer as children. The HTML kit uses this for a form shell with Save and Reset buttons.

Candidate key order

For each node, Surface builds this list (most specific first) and asks the kit for the first match:

schema $id
  → subject-root coordinate (<uri> or <uri>#/$defs/<name>)
  → widget.$kind
  → widget:<name> → name
  → format:<f> → f
  → const | enum | type | combinators

Aliases

createRegistryKit({ aliases }) adds one extra lookup hop. If a key A is not found, the kit retries with aliases[A] (one hop only). An explicit registration for the alias key wins over the alias target.

useSurface()

Renderers call this hook for context:

const { dataApi, validity, helpers, id, mode, view, schema, labels } = useSurface();

| Returned | What it does | | --- | --- | | dataApi.setData(next) | Replace this node's value | | dataApi.setChild(key, next) | Write one property or array index | | validity | Issues projected to this node's path. Use issuesAt(validity.issues, []) for leaf-level issues. | | helpers.fields() | Yield fields in annotation order, with sections | | helpers.layout() | "props" (default) or "stack" | | helpers.direction() | "vertical" (default) or "horizontal" | | labels | Current labels value (inherited) |

Data channel rules

  • Commit through setData and setChild. Do not keep a parallel local source of truth.
  • Wire formats belong on the channel: money in minor units, dates as RFC 3339.
  • Control value can differ from the wire format. Convert at the boundary.

SurfaceValidator

interface SurfaceValidator {
  validate(request: {
    id: string;
    schema: JsonSchemaDocument;
    data: unknown;
    schemaResolver: SchemaResolver;
  }): Promise<{ valid: boolean; issues: SurfaceIssue[] }>;
}

The recommended implementation is @rusl-labs/surface-ajv.

Related

| Doc | Why | | --- | --- | | Building kits | Full guide: registry, aliases, candidate keys, writing renderers, Root | | Annotations | Annotation model | | @rusl-labs/surface-html | HTML kit config and widgets | | @rusl-labs/surface-ajv | AJV validator adapter |