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/decorators

v0.1.0-alpha.8

Published

Decorator stubs for FormSpec CLI static analysis

Readme

@formspec/decorators

Decorator stubs for FormSpec CLI static analysis.

Quick Start

1. Install the packages:

npm install @formspec/decorators @formspec/cli

2. Create a decorated class:

// forms.ts
import { Label, Min, Max } from "@formspec/decorators";

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

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

3. Generate schemas:

formspec generate ./forms.ts UserForm -o ./generated
# Creates generated/UserForm/schema.json and ux_spec.json

Installation

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

Requirements

TypeScript configuration (tsconfig.json):

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

Package configuration (package.json):

{
  "type": "module"
}

Usage

import { Label, Min, Max, EnumOptions, ShowWhen, Group } from '@formspec/decorators';

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

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

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

  @Group("Preferences")
  @Label("Contact Method")
  contactMethod!: "email" | "phone";

  @ShowWhen({ field: "contactMethod", value: "email" })
  @Label("Email Address")
  email?: string;

  @ShowWhen({ field: "contactMethod", value: "phone" })
  @Label("Phone Number")
  phone?: string;
}

Generating Schemas

Build-Time Only

Generate JSON Schema and UI Schema files at build time:

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

This outputs static JSON files to ./generated/. No codegen step required.

Runtime Schema Generation

If you need JSON Schema or UI Schema at runtime in your program (e.g., dynamic form rendering, server-side generation), you have two options:

  1. Chain DSL - Works at runtime without any codegen step. See the Chain DSL documentation.

  2. Decorator DSL with codegen - If you prefer to keep using decorated classes, run codegen to preserve type information:

# Generate type metadata file
formspec codegen ./src/forms.ts -o ./src/__formspec_types__.ts
// Import the generated file at your application entry point
import './__formspec_types__.js';

// Now buildFormSchemas() has access to full type information
import { buildFormSchemas } from '@formspec/decorators';
import { UserRegistration } from './forms.js';

const { jsonSchema, uiSchema } = buildFormSchemas(UserRegistration);
// jsonSchema: { $schema: "...", type: "object", properties: {...}, required: [...] }
// uiSchema: { type: "VerticalLayout", elements: [...] }

Add formspec codegen to your build process to keep type metadata in sync.

Note: If you need to work with dynamically fetched schema data (schemas not known at build time), use the Chain DSL. It's the only option for dynamic schemas.

API Consistency

The buildFormSchemas() function provides the same return type as @formspec/build:

| DSL | Function | Returns | |-----|----------|---------| | Chain DSL | buildFormSchemas(form) | { jsonSchema, uiSchema } | | Decorator DSL | buildFormSchemas(Class) | { jsonSchema, uiSchema } |

This allows you to switch between DSLs without changing how you consume the schemas.

How It Works

These decorators are no-ops at runtime - they have zero overhead in your production code.

The FormSpec CLI uses TypeScript's compiler API to statically analyze your source files. It reads decorator names and arguments directly from the AST, without ever executing your code.

This means:

  • No reflection metadata required
  • No runtime dependencies
  • Works with any TypeScript configuration
  • Tree-shaking friendly

Available Decorators

Field Metadata

| Decorator | Purpose | Example | |-----------|---------|---------| | @Label(text) | Display label | @Label("Full Name") | | @Placeholder(text) | Input placeholder | @Placeholder("Enter name...") | | @Description(text) | Help text | @Description("Your legal name") |

Numeric Constraints

| Decorator | Purpose | Example | |-----------|---------|---------| | @Min(n) | Minimum value | @Min(0) | | @Max(n) | Maximum value | @Max(100) | | @Step(n) | Step increment | @Step(0.01) |

String Constraints

| Decorator | Purpose | Example | |-----------|---------|---------| | @MinLength(n) | Minimum length | @MinLength(1) | | @MaxLength(n) | Maximum length | @MaxLength(255) | | @Pattern(regex) | Regex pattern | @Pattern("^[a-z]+$") |

Array Constraints

| Decorator | Purpose | Example | |-----------|---------|---------| | @MinItems(n) | Minimum items | @MinItems(1) | | @MaxItems(n) | Maximum items | @MaxItems(10) |

Enum Options

| Decorator | Purpose | Example | |-----------|---------|---------| | @EnumOptions(opts) | Custom labels | @EnumOptions([{id: "us", label: "USA"}]) |

Auto-generated Options

When no @EnumOptions decorator is present, options are automatically generated from the union type with { id, label } format:

class UserForm {
  @Label("Status")
  status!: "draft" | "published" | "archived";
  // Auto-generates: [
  //   { id: "draft", label: "draft" },
  //   { id: "published", label: "published" },
  //   { id: "archived", label: "archived" }
  // ]
}

Record Shorthand

Use an object where keys are IDs and values are labels for a more concise syntax:

class UserForm {
  @Label("Country")
  @EnumOptions({
    us: "United States",
    ca: "Canada",
    uk: "United Kingdom"
  })
  country!: "us" | "ca" | "uk";
}

This is equivalent to the array format:

@EnumOptions([
  { id: "us", label: "United States" },
  { id: "ca", label: "Canada" },
  { id: "uk", label: "United Kingdom" }
])

Layout & Conditional

| Decorator | Purpose | Example | |-----------|---------|---------| | @Group(name) | Group fields | @Group("Contact Info") | | @ShowWhen(cond) | Conditional visibility | @ShowWhen({ field: "type", value: "other" }) |

TypeScript Configuration

For decorator support, ensure your tsconfig.json includes:

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

Note: The emitDecoratorMetadata flag is not required since these decorators don't use reflection.

License

UNLICENSED