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

@formspec/cli

v0.1.0-alpha.8

Published

CLI tool for generating JSON Schema and FormSpec from TypeScript classes

Readme

@formspec/cli

CLI tool for generating JSON Schema and JSON Forms UI Schema from TypeScript source files.

Installation

npm install @formspec/cli
# or
pnpm add @formspec/cli

Requirements

Package configuration (package.json):

{
  "type": "module"
}

TypeScript configuration (tsconfig.json):

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "module": "NodeNext",
    "moduleResolution": "NodeNext"
  }
}

Quick Start

# Generate schemas from a TypeScript class
formspec generate ./src/forms.ts MyClass -o ./generated

# Generate schemas from all FormSpec exports in a file (chain DSL)
formspec generate ./src/forms.ts -o ./generated

Commands

generate - Build-time Schema Generation

Generate JSON Schema and UI Schema files from TypeScript source files.

formspec generate <file> [className] [options]

Arguments:

  • <file> - Path to TypeScript source file
  • [className] - Optional class name to analyze (required for decorated classes)

Options:

  • -o, --output <dir> - Output directory (default: ./generated)
  • -c, --compiled <path> - Path to compiled JS file (auto-detected if omitted)

Examples:

# Generate from a decorated class
formspec generate ./src/forms.ts UserForm -o ./generated

# Generate from chain DSL exports (requires compiled JS)
tsc && formspec generate ./src/forms.ts -o ./generated

codegen - Runtime Type Metadata Generation

Generate a TypeScript file that patches decorated classes with type metadata, enabling runtime schema generation.

formspec codegen <files...> [options]

Arguments:

  • <files...> - TypeScript source files to analyze

Options:

  • -o, --output <file> - Output file (default: ./__formspec_types__.ts)

Examples:

# Generate type metadata for a single file
formspec codegen ./src/forms.ts -o ./src/__formspec_types__.ts

# Generate for multiple files
formspec codegen ./src/**/*.ts -o ./src/__formspec_types__.ts

Usage in code:

// Import once at application entry point (side-effect import)
import './__formspec_types__.js';

// Then use runtime schema generation
import { UserForm } from './forms.js';
import { buildFormSchemas } from '@formspec/decorators';

const { jsonSchema, uiSchema } = buildFormSchemas(UserForm);

When to use codegen:

  • You want to generate schemas at runtime (not build time)
  • You're using the decorator DSL (not chain DSL)
  • You need type information that TypeScript erases at compile time

Note: The chain DSL (@formspec/dsl) doesn't require codegen because it preserves type information at runtime.

Output Formats

FormSpec supports two output formats for UI schemas:

FormSpec Native Format (ux_spec.json)

Generated by the CLI's generate command. Uses FormSpec's internal representation:

{
  "elements": [
    {
      "_field": "text",
      "id": "name",
      "label": "Full Name",
      "required": true
    },
    {
      "_field": "enum",
      "id": "country",
      "label": "Country",
      "options": [
        { "id": "us", "label": "United States" },
        { "id": "ca", "label": "Canada" }
      ]
    }
  ]
}

JSON Forms Format (uiSchema)

Generated by buildFormSchemas() at runtime. Compatible with JSON Forms:

{
  "type": "VerticalLayout",
  "elements": [
    {
      "type": "Control",
      "scope": "#/properties/name",
      "label": "Full Name"
    },
    {
      "type": "Control",
      "scope": "#/properties/country",
      "label": "Country"
    }
  ]
}

Which Format to Use

| Use Case | Format | Generated By | |----------|--------|--------------| | Custom form renderer | FormSpec Native | formspec generate CLI | | JSON Forms library | JSON Forms | buildFormSchemas() | | Server-side generation | Either | Depends on your renderer |

Both formats are generated from the same FormSpec definition, ensuring consistency between build-time and runtime generation.

When to Use

Decorators + CLI vs Chain DSL

FormSpec offers two approaches for defining forms:

| Approach | Best For | Schema Generation | |----------|----------|-------------------| | Decorators + CLI | Class-based data models, DTOs | Build-time (static analysis) or runtime (with codegen) | | Chain DSL | Dynamic forms, runtime construction | Build-time or runtime (no code generation needed) |

Use decorators + CLI when:

  • Your forms map directly to TypeScript classes or DTOs
  • You prefer class-based modeling with validation constraints
  • You want type information inferred from TypeScript (no redundant type hints)
  • You need to generate schemas at build time for CI/CD pipelines

Use Chain DSL (@formspec/dsl) when:

  • Forms are constructed dynamically at runtime
  • Form definitions come from a database or API
  • Forms don't correspond to specific TypeScript types
  • You want runtime schema generation without a build step

Both approaches produce identical JSON Schema and UI Schema output.

Features

Static Type Analysis

The CLI uses the TypeScript Compiler API to statically analyze your source files. It automatically infers:

| TypeScript Type | JSON Schema | FormSpec Field | |-----------------|-------------|----------------| | string | { "type": "string" } | { "_field": "text" } | | number | { "type": "number" } | { "_field": "number" } | | boolean | { "type": "boolean" } | { "_field": "boolean" } | | "a" \| "b" \| "c" | { "enum": ["a", "b", "c"] } | { "_field": "enum", "options": [...] } | | string[] | { "type": "array", "items": {...} } | { "_field": "array" } | | { a: string } | { "type": "object", "properties": {...} } | { "_field": "object" } | | field?: T | not in required array | { "required": false } |

Decorator Recognition

The CLI recognizes decorators by name through static analysis. You don't need a specific decorator library - any decorator with a recognized name will work.

Supported Decorators

| Decorator | Purpose | Example | |-----------|---------|---------| | @Label(text) | Set field label | @Label("Full Name") | | @Placeholder(text) | Set placeholder text | @Placeholder("Enter name...") | | @Description(text) | Set field description | @Description("Your legal name") | | @Min(n) | Set minimum value | @Min(0) | | @Max(n) | Set maximum value | @Max(100) | | @MinLength(n) | Set minimum string length | @MinLength(1) | | @MaxLength(n) | Set maximum string length | @MaxLength(255) | | @MinItems(n) | Set minimum array items | @MinItems(1) | | @MaxItems(n) | Set maximum array items | @MaxItems(10) | | @Pattern(regex) | Set validation pattern | @Pattern("^[a-z]+$") | | @EnumOptions(opts) | Override enum options | @EnumOptions([{id: "us", label: "United States"}]) | | @ShowWhen(cond) | Conditional visibility | @ShowWhen({ field: "type", value: "other" }) | | @Group(name) | Group fields together | @Group("Contact Info") |

Using Decorators

Install the @formspec/decorators package:

npm install @formspec/decorators

Then use the decorators in your class:

// user-registration.ts
import { Label, Min, Max, EnumOptions } from "@formspec/decorators";

class UserRegistration {
  @Label("Full Name")
  name!: string;

  @Label("Email Address")
  email!: string;

  @Label("Age")
  @Min(18)
  @Max(120)
  age?: number;

  @Label("Country")
  @EnumOptions([
    { id: "us", label: "United States" },
    { id: "ca", label: "Canada" },
    { id: "uk", label: "United Kingdom" },
  ])
  country!: "us" | "ca" | "uk";
}

Run the CLI:

formspec generate ./src/user-registration.ts UserRegistration -o ./generated

Note: The decorators are no-ops at runtime with zero overhead. The CLI reads them through static analysis of your TypeScript source code.

FormSpec Chain DSL Support

The CLI also supports the FormSpec chain DSL. Export your FormSpec definitions and the CLI will generate schemas for them:

// forms.ts
import { formspec, field } from "@formspec/dsl";

export const ContactForm = formspec(
  field.text("name", { label: "Name", required: true }),
  field.text("email", { label: "Email", required: true }),
  field.text("message", { label: "Message" })
);
formspec generate ./src/forms.ts -o ./generated

Method Parameter Analysis

The CLI can detect InferSchema<typeof X> or InferFormSchema<typeof X> patterns in method parameters and use the referenced FormSpec to generate parameter schemas:

import { formspec, field, type InferFormSchema } from "@formspec/dsl";

export const ActivateParams = formspec(
  field.number("amount", { label: "Amount", min: 100 }),
  field.number("installments", { min: 2, max: 12 })
);

class PaymentPlan {
  status!: "active" | "paused" | "canceled";

  activate(params: InferFormSchema<typeof ActivateParams>): boolean {
    // ...
  }
}

The CLI will generate schemas for both the class fields and the method parameters.

Output Structure

generated/
├── ClassName/
│   ├── schema.json           # JSON Schema for class fields
│   ├── ux_spec.json          # JSON Forms UI Schema for form rendering
│   ├── instance_methods/
│   │   └── methodName/
│   │       ├── params.schema.json
│   │       ├── params.ux_spec.json  # (if FormSpec-based params)
│   │       └── return_type.schema.json
│   └── static_methods/
│       └── methodName/
│           └── ...
└── formspecs/
    └── ExportName/
        ├── schema.json
        └── ux_spec.json

Example Output

Given this TypeScript class:

import { Label, Min, Max, EnumOptions } from "@formspec/decorators";

class ContactForm {
  @Label("Full Name")
  name!: string;

  @Label("Email Address")
  email!: string;

  @Label("Age")
  @Min(18)
  @Max(120)
  age?: number;

  @Label("Country")
  @EnumOptions([
    { id: "us", label: "United States" },
    { id: "ca", label: "Canada" }
  ])
  country!: "us" | "ca";
}

Running formspec generate ./contact-form.ts ContactForm produces:

schema.json:

{
  "type": "object",
  "properties": {
    "name": { "type": "string", "title": "Full Name" },
    "email": { "type": "string", "title": "Email Address" },
    "age": { "type": "number", "title": "Age", "minimum": 18, "maximum": 120 },
    "country": { "enum": ["us", "ca"], "title": "Country" }
  },
  "required": ["name", "email", "country"]
}

ux_spec.json:

{
  "elements": [
    { "_field": "text", "id": "name", "label": "Full Name", "required": true },
    { "_field": "text", "id": "email", "label": "Email Address", "required": true },
    { "_field": "number", "id": "age", "label": "Age", "min": 18, "max": 120 },
    {
      "_field": "enum",
      "id": "country",
      "label": "Country",
      "required": true,
      "options": [
        { "id": "us", "label": "United States" },
        { "id": "ca", "label": "Canada" }
      ]
    }
  ]
}

CLI Reference

formspec generate <file> [className] [options]

Arguments:
  <file>        Path to TypeScript source file
  [className]   Optional class name (if omitted, generates from FormSpec exports only)

Options:
  -o, --output <dir>    Output directory (default: ./generated)
  -c, --compiled <path> Path to compiled JS file (auto-detected if omitted)
  -h, --help            Show help message

Aliases:
  formspec analyze      Same as 'generate' (backwards compatibility)

TypeScript Configuration

For decorator support, ensure your tsconfig.json includes:

{
  "compilerOptions": {
    "experimentalDecorators": true
  }
}

Note: The emitDecoratorMetadata flag is not required. The CLI performs static analysis and reads decorators directly from the AST without using reflection.

Troubleshooting

"Could not load compiled module" Warning

This warning appears when the CLI cannot find a compiled JavaScript version of your TypeScript file. This is expected if you haven't compiled your TypeScript yet.

The CLI will still work - it uses static TypeScript analysis which doesn't require compiled output. The warning only affects method parameters that use InferSchema<typeof X>, which require the FormSpec to be loaded at runtime.

To suppress this warning, compile your TypeScript first:

tsc
formspec generate ./src/forms.ts MyClass -o ./generated

Decorators Not Being Recognized

Ensure:

  1. Decorator names match exactly (case-sensitive): @Label, not @label
  2. Decorators are function calls: @Label("text"), not @Label
  3. The decorator is imported (even if it's a stub)

License

UNLICENSED