@ts-pf/codegen
v0.1.5
Published
Opt-in .d.ts codegen from catalog() for split-repo createClient
Maintainers
Readme
@ts-pf/codegen
Print a nested Contract .d.ts from a @ts-pf/docs catalog(). The frontend uses today's createClient<Contract>(link) — no second runtime, no as typeof contract, no tsc --dts of the backend.
Agent skill: skills/ts-pf-codegen/. Sync with npx skills experimental_sync -y. Runnable split-repo app: examples/codegen.
Types never come from fetch() or JSON.parse casts. The catalog is the spec; the .d.ts is the TypeScript contract.
import { catalog } from '@ts-pf/docs'
import { emit } from '@ts-pf/codegen'
import { writeFileSync } from 'node:fs'
const spec = catalog(contract, { prefix: '/rpc' })
writeFileSync('catalog.json', JSON.stringify(spec, null, 2))
writeFileSync('contract.d.ts', emit(spec))import { asResult, createClient } from '@ts-pf/client'
import { FetchLink } from '@ts-pf/client-http'
import type { Contract } from './contract.js'
const client = createClient<Contract>(new FetchLink({ url: '/rpc' }))
await client.planet.find({ id: 1 })
const result = await asResult(client.planet.find({ id: 1 }))
if (!result.ok && result.error.code === 'NOT_FOUND') {
result.error.data.id
}emit / catalogHash
import { emit, catalogHash, type EmitOptions } from '@ts-pf/codegen'
emit(catalog, {
name: 'Contract', // default
failOnUnavailable: false, // default; true throws on kind: 'unavailable'
banner: true, // catalogVersion + sha256 hash
})
catalogHash(catalog) // 'sha256:<hex>' of canonical JSONGenerated procedures are ContractProcedure<I, O, E> with Phantom Standard Schema leaves so InferErrorData / asResult still narrow error.code === 'NOT_FOUND'. Protocol errors stay out of the generated error map (ClientError already unions them). No-input is void. Streams are AsyncIterable<Item>.
docs() on a procedure becomes JSDoc on that generated member (description, summary, @deprecated). Tags stay OpenAPI grouping; other .meta() keys are not printed. JSDoc is on Contract members — ContractClient is a mapped type, so hover on client.planet.find may not show it.
Runtime policy (cache vs write, auth, …) stays on the catalog. Ship catalog.json next to contract.d.ts and look up procedures[].meta by path. createClient<Contract> is still type-only.
The generated file uses import type { ContractProcedure } from '@ts-pf/contract'. This package does not import contract at runtime.
CLI
ts-pf-codegen emit <catalog.json|-> [-o contract.d.ts] [--name Contract] [--fail-on-unavailable]
ts-pf-codegen pull <url> [-o contract.d.ts] [--lock catalog.lock.json]
ts-pf-codegen hash <catalog.json|->emit and hash read stdin when the path is -. Omit -o to write the .d.ts to stdout.
Commit contract.d.ts, or CI-run ts-pf-codegen pull and pin catalog.lock.json:
{
"url": "https://api.example.com/catalog.json",
"catalogVersion": 1,
"catalogHash": "sha256:…"
}Mismatch on pull exits non-zero. No registry in v1.
Serve the catalog in userland
Do not put this on FetchHandler. A GET under /rpc/... is a procedure miss / METHOD_NOT_ALLOWED, not a spec.
// Userland — not FetchHandler, not @ts-pf/codegen:
if (url.pathname === '/catalog.json') {
return Response.json(catalog(contract, { prefix: '/rpc' }))
}
const result = await handler.handle(req, { prefix: '/rpc', context })Not in this package
createClientFromCatalog- Folding into
@ts-pf/clientor@ts-pf/docs - OpenAPI-TS as the typed client (use
@ts-pf/openapifor the polyglot export) - Type-level
FromSchemaover imported JSON tsc --dtsof the live router- Serving catalog from FetchHandler
- Auth on
pull(later) - A schema registry
The catalog is the portable spec. @ts-pf/openapi remains the polyglot OpenAPI 3.1 export.
