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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@formlink/runtime

v0.1.0-alpha.8

Published

Updated: 2025‑10‑31

Readme

@formlink/runtime — Headless Runtime (alpha)

Updated: 2025‑10‑31

Single‑install, headless runtime for building delightful forms with deterministic validation, branching, navigation, persistence and uploads. React UI helpers and Devtools included. The canonical schema is embedded and exported via ./schema — install one package only.

Audience: App developers, LLM codegen, and UI engineers who need an authoritative, single source of truth. This doc is the authoritative reference for v1 alpha.

Table of contents

  • Why
  • Install
  • Exports & Subpaths
  • Prerequisites (shadcn/ui provider)
  • Host Setup Checklist
  • Quickstarts (Typeform, Classic)
  • Provider & UI Helpers
  • Devtools
  • Core API Reference (signatures)
  • Transports (fetch, formfiller, mock)
  • Schema (./schema) — types & shapes
  • Error Visibility Policy
  • Branching & Progress
  • File Uploads
  • Styles
  • SSR/Edge
  • FAQ

Why

  • Single source of truth for validation, visibility/branching, progress, persistence.
  • Deterministic execution (code) over AI at runtime; AI is for generation.
  • Single install: @formlink/runtime exports ./schema to prevent drift.

Install

  • pnpm add @formlink/runtime react react-dom
  • Optional UI helpers: a shadcn/ui component set is required; you can:
    • Map your own primitives via ShadCnProvider (recommended), or
    • Use our registry to pull UI sources (WIP), or
    • Use the future package @formlink/ui when it’s published (WIP — not on npm yet) Notes: ESM‑only; edge‑safe; peer deps react/react‑dom.

Exports & Subpaths

  • Core: @formlink/runtime
  • UI (React): @formlink/runtime/ui/react
  • Devtools: @formlink/runtime/devtools
  • Schema: @formlink/runtime/schema
  • Runtime CSS (precompiled): @formlink/runtime/ui/react/style.css
  • Tailwind preset: @formlink/runtime/tailwind-preset
  • Tailwind source sheet (optional): @formlink/runtime/styles/runtime-tailwind.css WIP packages
  • @formlink/ui — not yet published; until then, supply your own primitives or pull sources from our registry (see docs/registry/registry.json).
  • @formlink/chat — not yet published; conversational adapters will ship separately.

Prerequisites (shadcn/ui provider)

  • Supply shadcn/ui‑style primitives via ShadCnProvider (or equivalent components). These primitives should include their own styles (from your design system or, later, @formlink/ui).
  • If you don’t have a design system, import our minimal fallback CSS @formlink/runtime/ui/react/style.css to style runtime helpers (layout/progress/navigation) and ensure your primitives are styled.

shadcn/ui mapping (two approaches)

  • Map your own primitives via ShadCnProvider (see Provider & UI Helpers).
  • Or pull our ejectable sources from the registry (WIP) and map them; later you’ll be able to install @formlink/ui and map its components directly.

Host Setup Checklist

  • [ ] ShadCnProvider is mounted at app root with required primitives.
  • [ ] Import @formlink/runtime/ui/react/style.css (minimal CSS) OR ensure your design‑system CSS is present (or future @formlink/ui globals).
  • [ ] If using Typeform helpers, pages/components rendering them are client components ('use client').

Quickstart — Typeform style (reactive)

"use client";
import React from "react";
import { createRuntime } from "@formlink/runtime";
import {
  TypeFormTextInput,
  TypeFormContinueFooter,
  RuntimeProvider,
  ShadCnProvider,
} from "@formlink/runtime/ui/react";
import type { Form } from "@formlink/runtime/schema";

const form: Form = {
  id: "waitlist",
  title: "Join",
  questions: [
    {
      id: "email",
      title: "Work email",
      type: { name: "text", format: "email" },
      validations: { required: { value: true } },
    },
  ],
};

export default function View() {
  const rt = React.useMemo(
    () => createRuntime({ form, uiMode: "typeform" }),
    [],
  );
  const snap = React.useSyncExternalStore(
    rt.context.subscribe,
    rt.context.getSnapshot,
    rt.context.getSnapshot,
  );
  const qId =
    snap.currentId ?? snap.firstUnansweredId ?? snap.eligibleIds[0] ?? null;
  const q = qId ? rt.context.get.q(qId) : undefined;
  async function onContinue() {
    if (!qId) return;
    const res = await rt.actions.validate(qId);
    if (res.isValid) await rt.actions.next();
  }
  return (
    <RuntimeProvider runtime={rt} showDevtools>
      <ShadCnProvider
        components={
          {
            /* your primitives */
          } as any
        }
      >
        {snap.status === "idle" ? (
          <button onClick={() => rt.actions.start()}>Start</button>
        ) : q ? (
          <div>
            <h1>{rt.context.form.title}</h1>
            <h2>{q.title}</h2>
            <TypeFormTextInput
              type={(q.type as any).format}
              value={String(rt.context.get.value(qId) ?? "")}
              onChange={(v) => rt.actions.set(qId, v)}
            />
            <div className="text-red-500 text-sm">
              {rt.context.get.visibleError(qId)}
            </div>
            <TypeFormContinueFooter
              onClick={onContinue}
              isLoadingNext={snap.isSubmitting}
            />
          </div>
        ) : null}
      </ShadCnProvider>
    </RuntimeProvider>
  );
}

Quickstart — Classic (reactive)

"use client";
import React from "react";
import { createRuntime } from "@formlink/runtime";
import type { Form } from "@formlink/runtime/schema";

const form: Form = {
  id: "ex",
  title: "Example",
  questions: [
    { id: "name", title: "Name", type: { name: "text", format: "text" } },
  ],
};

export default function ClassicPage() {
  const rt = React.useMemo(
    () => createRuntime({ form, uiMode: "classic" }),
    [],
  );
  const snap = React.useSyncExternalStore(
    rt.context.subscribe,
    rt.context.getSnapshot,
    rt.context.getSnapshot,
  );
  return (
    <form
      onSubmit={async (e) => {
        e.preventDefault();
        await rt.actions.submit();
      }}
    >
      {snap.eligibleIds.map((qid) => (
        <div key={qid}>
          <label>{rt.context.get.q(qid)?.title}</label>
          <input
            value={String(rt.context.get.value(qid) ?? "")}
            onChange={(e) => rt.actions.set(qid, e.target.value)}
            onBlur={() => rt.actions.blur(qid)}
          />
          <div className="text-sm text-red-500">
            {rt.context.get.visibleError(qid)}
          </div>
        </div>
      ))}
      <button type="submit">Submit</button>
    </form>
  );
}

Provider & UI Helpers (React)

  • ShadCnProvider maps your primitives; required keys at minimum: Button, Input, Textarea, Label, Badge, ScrollArea, Separator, PopoverRoot/Trigger/Content/Anchor, CommandRoot/List/Item/Group/Empty/Input/Separator. See packages/runtime/docs/formlink-runtime-spec_v1_normative_only.md.
  • Exported helpers:
    • Typeform scaffolding: TypeFormLayout, TypeFormProgress, TypeFormQuestionHeader, TypeFormContinueFooter({ onClick, isLoadingNext? }), TypeFormNavigation({ onNext?, onPrevious?, canGoNext?, canGoPrevious?, isLoadingNext? }), TypeFormTransition.
    • Unified inputs: UnifiedDropdownSelect({ mode:'typeform'|'chat', options, value, onChange, onSubmit? … }), UnifiedDropdownMultiSelect({ options, value: T[], onChange, onSubmit? … }), UnifiedCountrySelect, UnifiedDatePicker, UnifiedPhoneInput, UnifiedFileUpload, InlineSelect, InlineMultiSelect, InlineRating, InlineRanking, InlineSignature.
    • RuntimeProvider (optional React context + Devtools). There is no public useRuntime export; subscribe via useSyncExternalStore as shown in quickstarts.
  • Fallback CSS: import '@formlink/runtime/ui/react/style.css' if your Tailwind pipeline does not scan the runtime.

Examples

  • docs/examples/universal-typeform.md
  • docs/examples/universal-classic.md
  • docs/examples/composed-react-wiring.md

Common Pitfalls

  • docs/pitfalls/start-button-no-op.md
  • docs/pitfalls/createRuntime-import.md
  • docs/pitfalls/mockTransport-alias.md
  • docs/pitfalls/useRuntime-export-missing.md
  • docs/pitfalls/question-undefined-title.md

Devtools

import { Devtools } from '@formlink/runtime/devtools'
<Devtools runtime={rt} mode="overlay" />
// or docked
<Devtools runtime={rt} mode="dock-left" dockWidth={360} />

Props: { runtime: RuntimeApi; position?: 'bottom-left'|'bottom-right'|'bottom-center'; fixed?: boolean; triggerClassName?: string; label?: string; mode?: 'overlay'|'dock-left'; dockWidth?: number }

Core API Reference (signatures)

// createRuntime
declare function createRuntime(config: RuntimeConfig): RuntimeApi;

// Config
interface RuntimeConfig {
  form: Form;
  transport?: RuntimeTransport;
  formfiller?: FormfillerTransportConfig;
  initialValues?: Partial<RuntimeValues>;
  initialStatus?: RuntimeStatus;
  initialCurrentId?: string | null;
  uiMode?: "typeform" | "classic";
}

// Context
interface RuntimeContext {
  readonly form: Form;
  readonly status: RuntimeStatus;
  readonly currentId: string | null;
  readonly eligibleIds: string[];
  readonly progress: { index: number; total: number; percent: number };
  readonly values: RuntimeValues;
  readonly errors: Record<string, string[]>;
  readonly firstUnansweredId: string | null;
  readonly unansweredIds: string[];
  readonly isValid: boolean;
  readonly isSubmitting: boolean;
  subscribe(listener: (s: RuntimeContextSnapshot) => void): () => void;
  getSnapshot(): RuntimeContextSnapshot;
  get: {
    q(qId: string): Question | undefined;
    value<T = unknown>(qId: string): T | undefined;
    error(qId: string): string | undefined;
    visibleError(qId: string): string | undefined;
  };
}

// Actions
interface RuntimeActions {
  start(): void;
  set(qId: string, value: unknown): void;
  blur(qId: string): void;
  next(): Promise<void>;
  prev(): void;
  goTo(qId: string): void;
  validate(qId: string): Promise<RuntimeValidationResult>;
  validateAll(): Promise<RuntimeValidationResult>;
  submit(): Promise<void>;
  reset(): void;
  savePartial(): Promise<void>;
  upload(qId: string, file: File | Blob): Promise<RuntimeUploadDescriptor>;
}

// Events
type RuntimeEventMap = {
  "status:change": { status: RuntimeStatus };
  "cursor:change": { currentId: string | null };
  "answer:set": { questionId: string; value: unknown };
  "visibility:change": { eligibleIds: string[] };
  "progress:change": { progress: RuntimeProgress };
  "submit:requested": { values: RuntimeValues };
  "submit:transport:start": { values: RuntimeValues };
  "submit:success": { values: RuntimeValues; result: RuntimeSubmissionResult };
  "submit:error": { error: unknown };
  "submit:transport:end": { result: RuntimeSubmissionResult | unknown };
  "upload:success": { questionId: string; descriptor: RuntimeUploadDescriptor };
  "upload:error": { questionId: string; error: unknown };
};

Transports

// Generic fetch transport
class RuntimeTransportError extends Error {
  constructor(message: string, status?: number);
}
interface FetchTransportOptions {
  baseUrl: string;
  submitPath?: string; // default: /forms/submit
  partialPath?: string; // default: /forms/save
  uploadPath?: string; // default: /files/upload
  headers?: Record<string, string>;
}
declare function fetchTransport(
  options: FetchTransportOptions,
): RuntimeTransport;

// Formfiller‑compatible
type FormfillerTransportOptions = {
  baseUrl?: string;
  formId: string;
  submissionId: string;
  formVersionId: string;
  isTestSubmission?: boolean;
  headers?: Record<string, string>;
};
declare function createFormfillerTransport(
  opts: FormfillerTransportOptions,
): RuntimeTransport;

// Mock (dev)
type MockTransportOptions = {
  delayMs?: number;
  saveDelayMs?: number;
  uploadDelayMs?: number;
  onSubmit?: (values: RuntimeValues) => Promise<unknown> | unknown;
  onSavePartial?: (values: RuntimeValues) => Promise<void> | void;
  onUpload?: (
    qId: string,
    file: File | Blob,
  ) => Promise<RuntimeUploadDescriptor> | RuntimeUploadDescriptor;
  generateObjectUrl?: boolean;
  revokeAfterMs?: number;
};
declare function createMockTransport(
  options?: MockTransportOptions,
): RuntimeTransport;

Schema (single‑install)

  • Import types and Zod schemas from @formlink/runtime/schema.
  • Discriminated union by type.name; validations include required/minLength/maxLength/pattern/minSelections/maxSelections/maxFiles/minDate/maxDate/allowedTypes/maxSize.
  • The runtime constructs a Zod object from your Form for on‑change and on‑submit validation.

Error Visibility Policy

  • typeform mode: reveal on Continue (next) if invalid; reveal all on submit; clear on valid change.
  • classic mode: reveal on blur; clear on valid change.
  • Use visibleError(qId) for rendering messages; do not compute visibility in the UI.

Branching & Progress

  • eligibleIds is the authoritative ordered list of visible questions; navigation respects it.
  • progress derives from eligibleIds and currentId.

File Uploads

  • Normalized descriptor: { url, name, size, mimeType?, metadata? }.
  • Use actions.upload(qId, file) or provide transport.upload.
  • UnifiedFileUpload integrates with the runtime (pass questionId and onFileUpload).

Styles

You have two options:

  1. Drop-in CSS bundle
    Import the prebuilt file once in your host app (e.g. _app.tsx, layout.tsx, or entry CSS):

    import "@formlink/runtime/ui/react/style.css";

    This includes the runtime layout utilities, tokens, and Tailwind utility classes used by our helpers.

  2. Integrate with your own Tailwind pipeline
    Extend your Tailwind config with the packaged preset and keep your existing design language:

    // tailwind.config.cjs
    const runtimePreset = require("@formlink/runtime/tailwind-preset");
    
    module.exports = {
      presets: [runtimePreset],
      content: [
        "./src/**/*.{ts,tsx}",
        // ...your content globs
      ],
      safelist: [
        ...(runtimePreset.safelist || []),
        // ...your extra safelist entries
      ],
    };

    Then include the source layer in your global stylesheet if you want to reuse our tokens:

    /* app.css */
    @import "@formlink/runtime/styles/runtime-tailwind.css";

    The safelist is already bundled in the preset, but you can also require("@formlink/runtime/tailwind-safelist") and merge manually if you prefer.

During development, run pnpm --filter @formlink/runtime dev:css to watch and rebuild the CSS bundle when editing runtime components locally.

SSR/Edge

  • Runtime is ESM‑only and uses browser‑safe APIs. React helpers are client‑side ('use client').

FAQ

  • Can I use it without React? Yes. The headless runtime is framework‑agnostic.
  • Does it support chat? Chat adapters will live in @formlink/chat (WIP); this package focuses on non‑chat flows.
  • Can I customize UI? Yes. Provide primitives via ShadCnProvider. A registry and @formlink/ui package (WIP) will make this turnkey.

License

MIT © Formlink