@formspec/build

中文文档
0.1.0-alpha.70发布于 2026/08/21MITTypeScriptESM + CJS构建来源已验证近 30 天下载 4280

Build tools to compile FormSpec forms to JSON Schema and UI Schema

原始使用文档
文档目录11 节
  1. Install
  2. Public Entry Points
  3. Chain DSL Generation
  4. Static Analysis
  5. Static Build Context
  6. Supported TSDoc Examples
  7. Extension-Aware Generation
  8. Handling Generation Failures
  9. Internal Entry Point
  10. Main Exports
  11. License

@formspec/build

Build-time schema generation for FormSpec.

This package covers:

  • Chain DSL to JSON Schema / UI Schema compilation
  • Static analysis of TypeScript classes, interfaces, and type aliases with TSDoc tags
  • Mixed-authoring schema generation
  • Extension-aware schema generation with custom vendor keywords

Install

pnpm add @formspec/build

Most app code can use formspec, but use @formspec/build directly when you need static analysis or lower-level generation APIs.

Public Entry Points

Entry pointPurpose
@formspec/buildPublic build APIs
@formspec/build/browserBrowser-safe chain-DSL and IR surface
@formspec/build/internalsUnstable low-level IR/analyzer APIs

Chain DSL Generation

import { buildFormSchemas } from "@formspec/build";
import { field, formspec } from "@formspec/dsl";

const form = formspec(
  field.text("name", { required: true }),
  field.enum("status", ["draft", "published"] as const)
);

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

Static Analysis

generateSchemas() is the main entry point for TSDoc-backed generation.

import { generateSchemas } from "@formspec/build";

const { jsonSchema, uiSchema } = generateSchemas({
  filePath: "./src/forms.ts",
  typeName: "ProductConfig",
});

generateSchemasFromClass() remains available when the input is definitely a class declaration.

import { generateSchemasFromClass } from "@formspec/build";

const result = generateSchemasFromClass({
  filePath: "./src/forms.ts",
  className: "ProductConfig",
});

Static Build Context

Use the static build context APIs when you need to inspect exports, declarations, or method signatures before deciding what schemas to generate.

Public helpers in this workflow:

  • createStaticBuildContext(filePath) - Create a reusable compiler-backed context from a file.
  • createStaticBuildContextFromProgram(program, filePath) - Reuse a host-owned ts.Program.
  • resolveModuleExport(context, exportName?) - Resolve any exported symbol, including functions and other non-schema declarations.
  • resolveModuleExportDeclaration(context, exportName?) - Resolve only schema-source declarations (class, interface, type alias).
  • resolveDeclarationMetadata(...) - Resolve metadata for named types, methods, and properties using FormSpec's active metadata policy.
  • generateSchemasFromDeclaration(...) - Generate from a resolved schema-source declaration.
  • generateSchemasFromParameter(...) - Generate from a method or function parameter declaration.
  • generateSchemasFromReturnType(...) - Generate from a method or function return type, unwrapping awaited Promise<T>-style returns before generation.
  • generateSchemasFromType(...) - Generate directly from a resolved ts.Type.

Use resolveModuleExportDeclaration(...) when your tooling wants to hand a resolved declaration straight to generateSchemasFromDeclaration(...). Use resolveModuleExport(...) when you need lower-level TypeScript access first, for example to inspect a function export and then generate schemas from one of its signature types.

import * as ts from "typescript";
import {
  createStaticBuildContext,
  generateSchemasFromDeclaration,
  generateSchemasFromParameter,
  generateSchemasFromReturnType,
  resolveDeclarationMetadata,
  resolveModuleExport,
  resolveModuleExportDeclaration,
} from "@formspec/build";

const context = createStaticBuildContext("./src/service.ts");
const serviceDeclaration = resolveModuleExportDeclaration(context, "PaymentService");

if (serviceDeclaration && ts.isClassDeclaration(serviceDeclaration)) {
  const submitMethod = serviceDeclaration.members.find(
    (member): member is ts.MethodDeclaration =>
      ts.isMethodDeclaration(member) &&
      ts.isIdentifier(member.name) &&
      member.name.text === "submit"
  );

  if (submitMethod?.parameters[0]) {
    const methodMetadata = resolveDeclarationMetadata({
      context,
      declaration: submitMethod,
    });
    const inputSchemas = generateSchemasFromParameter({
      context,
      parameter: submitMethod.parameters[0],
    });
  }
}

const inputDeclaration = resolveModuleExportDeclaration(context, "SubmitInput");
if (inputDeclaration) {
  const inputSchemas = generateSchemasFromDeclaration({
    context,
    declaration: inputDeclaration,
  });
}

const paymentSymbol = resolveModuleExport(context, "submitPayment");
const paymentDeclaration = paymentSymbol?.declarations?.find(ts.isFunctionDeclaration);
if (paymentDeclaration) {
  const outputSchemas = generateSchemasFromReturnType({
    context,
    declaration: paymentDeclaration,
  });
}

If you already own a ts.Program, use createStaticBuildContextFromProgram(program, filePath) instead of letting FormSpec create one. If your tool has already resolved a raw ts.Type or signature declaration, use generateSchemasFromType(...) or generateSchemasFromReturnType(...) directly.

This is the supported public path for build-time analysis workflows that used to require @formspec/build/internals.

Supported TSDoc Examples

export interface ProductConfig {
  /** @displayName Product Name @minLength 1 */
  name: string;

  /** @format email */
  supportEmail?: string;

  /** @placeholder Search products */
  query?: string;

  /** @minimum 0 @maximum 9999.99 */
  price: number;

  /** @uniqueItems */
  tags: string[];
}

Extension-Aware Generation

Static-analysis and mixed-authoring generation APIs accept extensionRegistry and vendorPrefix. Chain DSL generation accepts vendorPrefix, but not extensionRegistry.

import { createExtensionRegistry, generateSchemas } from "@formspec/build";

const registry = createExtensionRegistry([myExtension]);

const result = generateSchemas({
  filePath: "./src/forms.ts",
  typeName: "Invoice",
  extensionRegistry: registry,
  vendorPrefix: "x-acme",
});

Generation validates canonical IR before emitting schemas. Invalid inputs now fail generation with structured diagnostic codes surfaced in the thrown error.

Handling Generation Failures

Static generation now uses explicit error reporting on the main entry points:

  • generateSchemas({ ..., errorReporting: "throw" }) and generateSchemasFromProgram({ ..., errorReporting: "throw" }) keep the simple throw-on-error contract.
  • generateSchemas({ ..., errorReporting: "diagnostics" }) and generateSchemasFromProgram({ ..., errorReporting: "diagnostics" }) return structured diagnostics instead of throwing for analysis and validation failures.
  • generateSchemasBatch() and generateSchemasBatchFromProgram() continue to return per-target diagnostics across multiple targets.

Use the "throw" mode when you want "schema or failure" ergonomics. Use the "diagnostics" mode when you want to surface as much feedback as possible in one pass, especially in editor, CI, or migration tooling. The older generateSchemasDetailed() and generateSchemasFromProgramDetailed() wrappers remain available only as deprecated compatibility shims.

import { generateSchemas, generateSchemasBatch } from "@formspec/build";

try {
  const { jsonSchema, uiSchema } = generateSchemas({
    filePath: "./src/forms.ts",
    typeName: "Invoice",
    errorReporting: "throw",
  });
} catch (error) {
  const message = error instanceof Error ? error.message : String(error);

  if (message.includes("UNSUPPORTED_CUSTOM_TYPE_OVERRIDE")) {
    // An extension tried to override a TS global built-in such as Date or Array
  } else if (message.includes("SYNTHETIC_SETUP_FAILURE")) {
    // Extension setup failed before tag type-checking could run
  } else if (message.includes("TYPE_MISMATCH")) {
    // A tag was applied to an incompatible field or target type
  }

  throw error;
}

const detailed = generateSchemas({
  filePath: "./src/forms.ts",
  typeName: "Invoice",
  errorReporting: "diagnostics",
});

if (!detailed.ok) {
  for (const diagnostic of detailed.diagnostics) {
    console.error(`${diagnostic.code}: ${diagnostic.message}`);
  }
}

const batch = generateSchemasBatch({
  targets: [
    { filePath: "./src/forms.ts", typeName: "Invoice" },
    { filePath: "./src/forms.ts", typeName: "PaymentTerms" },
  ],
});

for (const result of batch) {
  if (!result.ok) {
    console.error(
      `${result.typeName} failed: ${result.diagnostics.map((diagnostic) => diagnostic.code).join(", ")}`
    );
  }
}

The most relevant codes for extension-backed static analysis failures are:

  • UNSUPPORTED_CUSTOM_TYPE_OVERRIDE for extension custom types that conflict with unsupported TypeScript global built-ins.
  • SYNTHETIC_SETUP_FAILURE for invalid or conflicting extension custom type registrations and other synthetic compiler setup failures.
  • TYPE_MISMATCH for normal tag-on-type incompatibilities in author source.
  • TYPE_NOT_FOUND when a requested exported target cannot be resolved.
  • UNSUPPORTED_ROOT_TYPE and DUPLICATE_ROOT_PROPERTIES when a requested type alias cannot be treated as a schema root.
  • PROGRAM_CONTEXT_FAILURE when the package cannot create or reuse a TypeScript program for the requested file.

As a rule of thumb, UNSUPPORTED_CUSTOM_TYPE_OVERRIDE and SYNTHETIC_SETUP_FAILURE indicate extension configuration problems, while TYPE_MISMATCH usually indicates an authoring error in the analyzed source.

Internal Entry Point

Low-level canonical IR generators, analyzer primitives, and validation helpers are intentionally no longer exported from the package root. If you need those unstable internals inside the monorepo, import them from @formspec/build/internals.

Main Exports

  • buildFormSchemas(form, options?)
  • generateJsonSchema(form, options?)
  • generateUiSchema(form)
  • writeSchemas(form, options)
  • generateSchemas(options)
  • generateSchemasFromClass(options)
  • generateSchemasFromProgram(options)
  • generateSchemasBatch(options)
  • generateSchemasBatchFromProgram(options)
  • buildMixedAuthoringSchemas(options)
  • createExtensionRegistry(extensions)

writeSchemas() is the chain-DSL convenience wrapper for writing emitted files. Extension registries apply to the static-analysis and mixed-authoring generation flows above, not to writeSchemas().

License

This package is part of the FormSpec monorepo and is released under the MIT License. See LICENSE for details.

版本与发布记录

@formspec/build 共发布 57 个版本,首次发布于 2026/01/29;过去 12 个月发布了 57 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.1.0-alpha.70latest2026/08/212 个月前
0.1.0-alpha.632026/04/246 个月前
0.1.0-alpha.612026/04/236 个月前
0.1.0-alpha.592026/04/226 个月前
0.1.0-alpha.582026/04/216 个月前
0.1.0-alpha.572026/04/206 个月前

其他发布通道:alpha → 0.1.0-alpha.1。

关于 @formspec/build 的常见问题

@formspec/build 是什么?

Build tools to compile FormSpec forms to JSON Schema and UI Schema

如何安装 @formspec/build?

在项目目录运行 npm install @formspec/build,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:typescript(>=5.7.3 <7)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。它还提供命令行工具 formspec-build,可以用 npx formspec-build 直接运行。

@formspec/build 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@formspec/build 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@formspec/build 可以商用吗?

@formspec/build 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@formspec/build 的下载量有多少?

根据 npm 官方下载统计,@formspec/build 在 2026/10/16 – 2026/11/14 这 30 天内被下载 4,280 次,日均约 143 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@formspec/build 还在维护吗?

最新版本 0.1.0-alpha.70 发布于 2026/08/21(2 个月前);至今共发布 57 个版本,其中过去 12 个月发布了 57 个;首次发布于 2026/01/29。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。