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

@accorudo/openapi

v0.0.2

Published

Extract Accorudo contract bundles into an intermediate representation and emit OpenAPI 3.1 specs.

Readme

@accorudo/openapi

Extracts an Accorudo contract bundle (an api.ts route registry plus its routes/ and models/) into an intermediate representation (IR), and emits that IR as an OpenAPI 3.1 document.

This package is a programmatic API only — the CLI lives in @accorudo/cli (init / import / export / sync), which wraps this package's parseOpenApi/generateContracts (import direction) and extractBundle/emitOpenApi (export direction).

Why an IR

A Route<...> alias only resolves to concrete method / path / pathParams / query / body / response values after TypeScript's checker resolves its generics (CombineRouteFragments, indexed access, imported fragments). Regex or AST-only parsing can't do that, so extraction goes through ts-morph and the real type checker.

Rather than emitting OpenAPI directly off the AST, extraction produces a ContractBundle — operations plus components.{schemas,parameters,responses,requestBodies,securitySchemes} — and OpenAPI is just one emit target on top of that IR. This keeps import, export, and future codegen from re-implementing the same walk.

Install

pnpm add @accorudo/openapi

Usage

import { extractBundle, emitOpenApi, stringifyOpenApi } from "@accorudo/openapi";

const bundle = extractBundle("./contracts/main", { name: "main" });
const doc = emitOpenApi(bundle, { info: { title: "Main API", version: "1.2.0" } });

console.log(stringifyOpenApi(doc, "yaml")); // or "json"

The reverse direction — generate contract files from an OpenAPI spec:

import { parseOpenApi, parseOpenApiText, generateContracts } from "@accorudo/openapi";
import fs from "node:fs";

const doc = parseOpenApiText(fs.readFileSync("./openapi/main.yaml", "utf8"), "yaml");
const bundle = parseOpenApi(doc, { name: "main" });

for (const file of generateContracts(bundle)) {
  console.log(file.path); // "api.ts", "routes/widget.ts", "models/widget.ts", ...
}

extractBundle(bundleDir, options) reads bundleDir/api.ts (configurable via entryFile), auto-detects the route registry type exported from it (an object type whose every property structurally has method and path — this is what lets it skip over a co-exported Api<Routes> alias), and walks every operation and every @openapi.component-tagged type reachable under bundleDir.

If the bundle isn't self-contained (it imports app types via ~/types/* or similar), pass tsConfigFilePath so those resolve:

extractBundle("./contracts/main", { tsConfigFilePath: "./tsconfig.json" });

By default, components.schemas entries that no operation actually reaches are dropped from the result. Pass includeUnusedComponents: true to keep them.

Annotations

Extraction and emission are driven by @openapi.* JSDoc tags on route aliases, model types, and their properties — @openapi.component, @openapi.type/@openapi.format, @openapi.nullable/@openapi.required, @openapi.inline, @openapi.response CODE [Ref], @openapi.tag, @openapi.security, and more. See OpenAPI annotations for the full reference — this package implements that vocabulary.

Reusable @openapi.component parameters types resolve to $refs automatically wherever they're used as a query/path property's type (e.g. limit?: LimitParameter inside a *QueryParams type) — @openapi.parameter Name on the route alias is only needed for a parameter that isn't otherwise a literal property of pathParams/query (e.g. a shared header).

Public API

| Export | Purpose | |---|---| | extractBundle(bundleDir, options?) | Walk a contract bundle into a ContractBundle IR | | filterBundleByTags(bundle, tags) | Drop operations (and now-unreachable components) not matching any of tags | | emitOpenApi(bundle, options?) | Convert IR into a plain OpenApiDocument object | | parseOpenApi(doc, options?) | Convert a raw OpenApiDocument into a ContractBundle IR | | generateContracts(bundle, options?) | Convert IR into { path, content }[] TypeScript contract files. Pass importStyle: "umbrella" for accorudo/* imports, or "scoped" (default) for @accorudo/*. The CLI picks this automatically from your package.json. | | stringifyOpenApi(doc, "yaml" \| "json") | Serialize an OpenApiDocument | | parseOpenApiText(text, "yaml" \| "json") | Parse spec text into an OpenApiDocument | | parseOpenApiJSDoc + getFlag/getValue/getList/getPair(s)/getMapping/getNumber/omitTag | Low-level @openapi.* tag parsing, if you're building your own emitter | | walkType, buildComponentIndex, WalkContext | The schema walker, for advanced/custom extraction | | AccorudoOpenApiError | Thrown on unresolvable registries or bundle type errors |

All IR types (ContractBundle, OperationNode, SchemaNode, ...) are exported from the package root.

Scope

  • ✅ contracts/ → ContractBundle → OpenAPI (export)
  • ✅ OpenAPI → ContractBundle (import) — parseOpenApi
  • ✅ ContractBundle → contracts/ (codegen) — generateContracts
  • ❌ CLI — lives in @accorudo/cli (separate package)

Known limitations of parseOpenApi / generateContracts

  • allOf is flattened into a merged object node — composition semantics are not preserved.
  • A formatless integer schema is parsed with a synthetic format: "int32" so it round-trips back to "integer" on export, since the IR has no separate "integer" kind.
  • Inline (non-$ref) secondary responses are promoted into synthesized, mechanically-named components.responses entries during codegen, because the @openapi.response CODE Ref annotation can only reference a named response.
  • Inline header/cookie parameters are promoted into synthesized components.parameters entries during parsing, since OperationNode only has slots for path/query parameters.
  • Model file grouping (models/<tag>.ts vs models/common.ts) is a deterministic single-tag-reachability rule and may place a schema differently than a human would.
  • No CombineRouteFragments synthesis (always one flat RouteFragment per path) and no response-wrapper-generic inference (Success<T>/CollectionSuccess<T>) — generated names and shapes are mechanical. Review generated files before committing, per the CLI's own import guidance.

Development

pnpm --filter @accorudo/openapi test   # vitest
pnpm --filter @accorudo/openapi build  # tsc -> dist/

test/fixtures/widget-bundle is a small hand-written bundle exercising components, $ref vs @openapi.inline, path/query parameters (including reusable parameter components), multiple response codes, security schemes, and a multipart route. test/emit.spec.ts extracts it and diffs the result against test/fixtures/widget-bundle.expected.yaml.