Codegen Artifact Generation Scripts
Reference for the two scripts that turn openapi/primitive-api.yaml into api-core's generated TypeScript artifacts: the normalized codegen spec, the embedded OpenAPI constant, the operation manifest, and the post-generation import fixups.
Two scripts in packages/api-core/scripts/ turn the hand-written OpenAPI source into the generated TypeScript artifacts that @primitivedotdev/api-core exports. Both run as part of pnpm generate in packages/api-core, and both are wired into the root make node-generate target described in Regenerating SDK Code from the OpenAPI Spec.
For the overall pipeline shape (why api-core exists, how sdk-node and cli-node bundle it), see Architecture: Shared Codegen Pipeline. For the OpenAPI 3.1 → 3.0.3 normalization rules applied here, see OpenAPI Spec Normalization and Codegen Artifacts.
generate-openapi-artifacts.ts#
Reads openapi/primitive-api.yaml (the authored OpenAPI 3.1 source of truth) and writes three files. Run it directly with:
cd packages/api-core
pnpm generate:openapi
Internally this is tsx scripts/generate-openapi-artifacts.ts, declared in packages/api-core/package.json.
Inputs and outputs#
| Path | Role |
|---|---|
openapi/primitive-api.yaml | Input. Parsed with the yaml package. |
openapi/primitive-api.codegen.json | Output. Normalized OpenAPI 3.0.3 JSON for the code generators. |
packages/api-core/src/openapi/openapi.generated.ts | Output. Exports openapiDocument, the raw (non-normalized) spec as a TypeScript constant. |
packages/api-core/src/openapi/operations.generated.ts | Output. Exports operationManifest, PrimitiveOperationManifest, and PrimitiveParameterManifest. |
What normalization does#
Before writing the codegen JSON, the script runs normalizeForCodegen over a deep clone of the raw spec:
- Nullable-type unwrapping: an OpenAPI 3.1
type: ["string", "null"]array becomestype: "string"plusnullable: true, since @hey-api/openapi-ts's 3.0.3-oriented generator does not consume the 3.1 array form. - Content-type remapping for binary bodies:
message/rfc822andapplication/gziprequest/response content are rekeyed toapplication/octet-streamso the generator treats them as binary payloads. - The result is stamped
openapi: "3.0.3"and written toprimitive-api.codegen.json.
The raw (non-normalized) spec is written unchanged into openapi.generated.ts as the openapiDocument constant. That constant is what CLI tooling and the operation manifest infrastructure serve as the literal OpenAPI document, distinct from the codegen-only JSON.
Never hand-edit primitive-api.codegen.json. It's a generated build artifact; edit openapi/primitive-api.yaml and re-run the script. See OpenAPI spec authoring version vs. codegen consumption.
Building the operation manifest#
For every HTTP method (delete, get, head, options, patch, post, put) on every path in the raw spec, the script emits one PrimitiveOperationManifest entry when the operation declares an operationId:
| Field | Description |
|---|---|
command | kebab-case form of operationId (e.g. sendEmail → send-email). |
operationId / sdkName | The original OpenAPI operationId. |
method | Uppercased HTTP method. |
path | The OpenAPI path template. |
tag / tagCommand | First declared tag, and its kebab-case form. |
summary / description | Copied from the operation, or null. |
pathParams / queryParams | Resolved parameters (merging path-item-level and operation-level parameters, $refs included), split by in: path / in: query. Each includes name, type, required, description, enum, default, minimum, maximum. |
bodyRequired | true when requestBody.required is set. |
hasJsonBody | true when requestBody.content["application/json"].schema exists. |
requestSchema | The request body JSON Schema with every $ref inlined, or null. |
responseSchema | The JSON Schema for the data property of the 200/201 response envelope, $refs inlined, or null if no 200/201 JSON response exists. |
binaryResponse | true when any response declares application/octet-stream, application/gzip, message/rfc822 content, or a format: binary schema. |
The manifest is sorted by tagCommand, then command.
How requestSchema and responseSchema are resolved
Both fields use inlineSchemaRefs, a recursive walker that replaces every $ref with the resolved schema from components/schemas or components/parameters, breaking cycles by leaving a bare { $ref: "..." } if the same ref is seen twice on one path.
responseSchema specifically unwraps the repo's allOf: [SuccessEnvelope, { properties: { data: <schema> } }] idiom (or the ListEnvelope equivalent): it looks for an allOf member with a data property and returns that property's schema directly, so callers get the payload shape rather than the { success, data, meta } wrapper. If no member matches that shape, it falls back to the full inlined response schema rather than returning null, on the reasoning that an imperfect schema is more useful to an agent than no schema.
This is what powers primitive list-operations | jq '.[] | select(.command == "send-email") | .requestSchema' and the CLI's describe command; see Operation Manifest Reference and Direct API Access and Generic Commands.
When to re-run it#
Re-run generate-openapi-artifacts.ts (directly, or via pnpm generate / make node-generate) whenever openapi/primitive-api.yaml changes: new operations, changed parameters, changed request/response schemas, or changed tags. Downstream consumers (operationManifest, openapiDocument, and the codegen JSON that generate:api reads next) are all stale until this runs.
fix-generated-api-imports.ts#
fix-generated-api-imports.ts post-processes the @hey-api/openapi-ts output under packages/api-core/src/api to repair three known generator gaps: extensionless relative imports, unconditional Content-Type headers on optional-body operations, and a widened MemoryJsonValue type. It runs as the second half of pnpm generate:api:
cd packages/api-core
pnpm generate:api
That script is openapi-ts -f openapi-ts.config.ts && tsx scripts/fix-generated-api-imports.ts, so the fixup always runs immediately after generation, never standalone against stale output.
1. Add .js extensions to relative imports#
@hey-api/openapi-ts emits extensionless relative imports (from "./types"), which fail under Node's ESM resolution. For every from "..." and dynamic import("...") specifier starting with ./ or ../:
- Leaves
.js/.jsonspecifiers untouched. - If the specifier resolves to a directory containing
index.ts, rewrites it to<specifier>/index.js. - Otherwise rewrites it to
<specifier>.js.
2. Guard optional-body Content-Type headers#
@hey-api/openapi-ts unconditionally emits:
headers: {
'Content-Type': 'application/json',
...options.headers
}
on every operation with a request body in the spec, even when the body is optional. For the optional-body operations (testFunction, cli_logout, start_cli_login, search_emails), this sends the header with no payload when the caller omits the body, which is wrong on the wire. The fixup rewrites the pattern to:
headers: {
...(options.body !== undefined && { 'Content-Type': 'application/json' }),
...options.headers
}
so the header only appears when a body is actually present. Required-body operations are unaffected, since the type system guarantees body is always defined there. This is the same class of bug documented for the Go and Python generators; see Non-TypeScript Codegen: Go and Python Clients and Codegen Troubleshooting.
3. Repair the MemoryJsonValue type#
@hey-api/openapi-ts currently widens the OpenAPI 3.1 type: "null" branch of the recursive MemoryJsonValue JSON-value schema to unknown, which widens the entire generated type alias. The fixup detects export type MemoryJsonValue = ... in the generated output and replaces it with the exact type:
export type MemoryJsonValue = string | number | boolean | Array<MemoryJsonValue> | {
[key: string]: MemoryJsonValue;
} | null;
If the generated file's shape ever changes enough that the replacement pattern no longer matches, the script throws loudly (Unable to repair generated MemoryJsonValue type in ${file}...) instead of silently leaving the broken type in place. That failure is the signal that fixMemoryJsonValueType needs updating before the next publish. See Memory Value Validation Helper for the runtime counterpart, isMemoryJsonValue.
When to re-run it#
Never run it standalone. It only makes sense immediately after openapi-ts regenerates packages/api-core/src/api, which is why generate:api chains both steps. If pnpm generate:api (or make node-generate) fails partway or you regenerate with a different openapi-ts version and start seeing raw unknown types on MemoryJsonValue or missing .js extensions in generated imports, re-run pnpm generate:api from packages/api-core rather than invoking the fixup script alone against stale generator output.
Running both together#
cd packages/api-core
pnpm generate
pnpm generate runs generate:openapi then generate:api in sequence, which is exactly what make node-generate invokes from the repo root. Always commit the regenerated files alongside the source spec/schema change, per Regenerating SDK Code from the OpenAPI Spec.
Was this page helpful?