npm.io
0.3.1 • Published 12h ago

@openmirai/typeforge

Licence
MIT
Version
0.3.1
Deps
0
Vulns
0
Weekly
0
Stars
1

Typeforge logo

@openmirai/typeforge

Headless OpenAPI / Swagger → TypeScript codegen. The CLI is typeforge. It reads a spec, writes typed route enums, request types, and HTTP caller functions, and never talks to a network.

You own http.ts (the HTTPFetch adapter). Generated files import that adapter — they do not invent axios/fetch calls inline.

What it generates

For each source (a named API, e.g. atlas), under <apiRoot>/<source>/generated/:

Output Role
types/**/*.d.ts Params, body, and response types per operation
functions/**/*.ts Typed callers (getWidgets, …)
routes.ts Routes string map + RouteTargets enum (name configurable)
runtime.ts Re-exports HTTPFetch, httpFetch (if singleton), routes
base.ts BaseResponse<T> when the spec uses a response envelope (or base.d.ts for split declaration output)

Optional:

  • TanStack Query — set tanstackQuery: true in source.ts and add <apiRoot>/query-scope.ts.
  • Zod — wrap a schema with createZodValidator from @openmirai/typeforge/validation/zod and pass it as config.validateResponse.

Install

Requires Node.js 24+ (LTS). Use any package manager.

Package manager Install
npm npm install --save-dev @openmirai/typeforge
pnpm pnpm add -D @openmirai/typeforge
yarn yarn add -D @openmirai/typeforge
bun bun add -d @openmirai/typeforge

Axios is an optional peer. Install axios only if you use --client axios.

Add a script so every package manager resolves the CLI from node_modules/.bin:

{
  "scripts": {
    "generate:types": "typeforge generate --all"
  }
}

Then run npm run generate:types, pnpm run generate:types, yarn generate:types, or bun run generate:types.

CLI usage

Prefer the package.json script above. To invoke the binary directly:

Command npm pnpm yarn bun
Init a source npx typeforge init --source atlas --client axios pnpm exec typeforge init --source atlas --client axios yarn typeforge init --source atlas --client axios bunx typeforge init --source atlas --client axios
Generate one source npx typeforge generate --source atlas pnpm exec typeforge generate --source atlas yarn typeforge generate --source atlas bunx typeforge generate --source atlas
Generate all sources npx typeforge generate --all pnpm exec typeforge generate --all yarn typeforge generate --all bunx typeforge generate --all
Drift check (CI) npx typeforge generate --all --check pnpm exec typeforge generate --all --check yarn typeforge generate --all --check bunx typeforge generate --all --check
Subcommand Purpose
init Scaffold http.ts, source.ts, known-types.ts
generate Write generated files
check Same as generate --check — exit 1 if output would change
accept-base Update generated base.ts (base.d.ts for split declaration output) and patch models.ts BaseResponse

--check and --accept-base cannot be combined. See docs/cli.md for the full command reference.

How the flow works

init  →  source.ts + http.ts  →  resolve spec  →  generate  →  typed callers
1. Init a source
typeforge init --source atlas --client axios
typeforge init --source orbit --client fetch --layout packages

--client is axios | fetch | custom. --layout is monolith (default, apiRoot = src/api) or packages (apiRoot = packages/utils/src/api).

Init creates (if missing):

  • typeforge.json with apiRoot
  • <apiRoot>/http.ts — your HTTPFetch implementation
  • <apiRoot>/known-types.ts — optional schema → local type mapping
  • <apiRoot>/<source>/source.ts — per-API config (type-safe template)
  • <apiRoot>/<source>/generated/ directory

Existing files are skipped.

2. Configure source.ts

Use defineSourceConfig for autocomplete and compile-time checks:

import { defineSourceConfig } from "@openmirai/typeforge";

export default defineSourceConfig({
  spec: "./specs/acme.json",
  functionsDir: "packages/utils/src/api/routes/atlas",
  typesDir: "packages/types/src/api/atlas",
  pathPrefix: "/api/acme/v3",
  stripApiPrefix: true,
  routeEnumName: "RouteTargets",
  generationMode: "authoritative",
  naming: "path",
  ignorePaths: [],
  maxRenderDepth: 50,
  resolveMapKeyRefs: true,
  tanstackQuery: false,
  queryExtends: {
    page: "page",
    limit: "limit",
    sortBy: "sortBy",
    sortOrder: "sortOrder",
    paginationTypeName: "OffsetLimitQuery",
    paginationImportPath: "./pagination",
    sortTypeName: "SortParams",
    sortImportPath: "./pagination",
  },
});

Plain export default { ... } still works; the CLI reads config fields from the file at generate time.

Re-exported types from the package root:

  • SourceConfig, QueryExtendsConfig, GenerationMode, NamingStrategy
  • defineSourceConfig(config) — identity helper for typed source.ts
Field Meaning
spec Project-relative spec path (used when no --spec / env override)
functionsDir Project-relative function output directory (defaults to the source's generated/functions)
typesDir Project-relative type output directory (defaults to the source's generated/types; a generated base.d.ts is placed beside this directory when customized)
pathPrefix Only generate operations under this prefix (e.g. /api/acme/v3)
ignorePaths Extra paths to skip
stripApiPrefix Strip a leading /api segment from route enum member names
routeEnumName Enum name (default RouteTargets)
generationMode authoritative (overwrite routes) or merge (keep extra enum members)
naming path or operationId for function names
queryExtends Fold page/limit/sort query params into shared pagination types
tanstackQuery Emit Query helpers when query-scope.ts exists
importBase Force import prefix for generated function files (overrides tsconfig aliases)
maxRenderDepth / resolveMapKeyRefs Schema renderer limits
unwrapResponseData Emit an envelope's data schema as the operation response type when the project's HTTPFetch already unwraps envelopes
3. Spec resolution (first match wins)
  1. --spec <path>
  2. Env OPENAPI_SPEC_<KEY> — source key uppercased, hyphens → underscores
  3. spec in that source’s source.ts
  4. typeforge.local.json (gitignored) map of { "<source>": "<path>" }
  5. Committed snapshot <apiRoot>/<source>/spec.json
4. Envelope modes

Inferred from success response schemas. Details: docs/envelope.md.

Mode When Types
shared One envelope shape (data / success / message) BaseResponse<Unwrapped>
raw No shared envelope Spec schema as-is
mixed Some ops have data, others do not Unwrap per operation when data exists

Set unwrapResponseData: true when the project's injected HTTPFetch normalizes successful envelope bodies before returning { data }. Every operation whose success schema is recognized as an API envelope then receives its data payload type. Data-only objects and business payloads that also contain success remain raw. Metadata-only envelopes without a data field receive the null type, matching clients that normalize an omitted payload to null. The default remains envelope-preserving and is compatible with the bundled Axios and Fetch adapters.

5. HTTPFetch (http.ts)

Adapters implement HTTPFetch from @openmirai/typeforge/http (or the axios/fetch adapter packages). Methods return Promise<{ data: TResponse }>.

  • If http.ts exports httpFetch, generated functions call that singleton.
  • Otherwise they take props.http: HTTPFetch (injected).
6. Path-alias aware imports

Generated function files import types and runtime, and generated response types import the generated base declaration (base.ts in monolith output, or base.d.ts for split declaration output), using:

  1. importBase in source.ts, if set
  2. Else compilerOptions.paths from the nearest ancestor tsconfig.json with path aliases, starting at the corresponding functionsDir or typesDir
  3. Else relative paths (../../runtime)

Where files go

typeforge.json:

{ "apiRoot": "packages/utils/src/api" }

You can also set "typeforge": { "apiRoot": "..." } in package.json. The JSON file wins.

Monolith (--layout monolith, default):

src/api/http.ts
src/api/known-types.ts
src/api/models.ts                 # optional BaseResponse drift check
src/api/query-scope.ts            # optional TanStack
src/api/atlas/source.ts
src/api/atlas/spec.json           # optional snapshot
src/api/atlas/generated/…

Packages layout (--layout packages): typical placement is packages/utils/src/api/<source>/.

Set functionsDir and typesDir when callers and declarations belong in different packages. Relative imports continue to work without aliases; when a nearby tsconfig.json maps both output roots, deep generated imports use those aliases automatically.

Zod (optional)

import { createZodValidator } from "@openmirai/typeforge/validation/zod";
import { widgetListSchema } from "./widget-list";

await getWidgets({
  params: { page: 1, limit: 20 },
  config: { validateResponse: createZodValidator(widgetListSchema) },
});

Releasing

Publishes go through npm Trusted Publishing (GitHub Actions OIDC). Do not npm publish from a laptop.

Value
npm package @openmirai/typeforge
GitHub repo openmirai/typeforge
Workflow .github/workflows/publish.yml
Tag v* (e.g. v0.1.3)

Develop this repo

This repository uses pnpm for its own CI. Consumers are not required to use pnpm.

pnpm install
pnpm verify   # format, lint, typecheck, build, coverage

Test layout: test/README.md.