{"schema_version":"1.0","publisher":"Primitive SDKs","canonical_url":"https://test.abhinandan.one/codegen-workflow/api-core-generation-scripts","markdown_url":"https://test.abhinandan.one/codegen-workflow/api-core-generation-scripts.md","article":{"id":"13198b07-305a-4d04-a3d5-38fa3da93a67","article_slug":"api-core-generation-scripts","parent_article_slug":"codegen-workflow","parent_article_title":"Regenerating SDK Code from the OpenAPI Spec","kind":"reference","published_at":"2026-08-11T18:55:02.623878+00:00","keywords":["generate-openapi-artifacts.ts","fix-generated-api-imports.ts","operationManifest","primitive-api.codegen.json","openapiDocument","pnpm generate:openapi"],"meta_description":"generate-openapi-artifacts.ts builds the codegen JSON spec, the embedded OpenAPI constant, and the operation manifest; fix-generated-api-imports.ts repairs the hey-api output afterward.","og_image_url":null,"source_file_paths":["packages/api-core/scripts/generate-openapi-artifacts.ts","packages/api-core/scripts/fix-generated-api-imports.ts"],"recording_id":null,"replayable":false,"task_name":"Codegen Artifact Generation Scripts","category":"API Core / OpenAPI Generation","summary":null,"description":"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.","content_kind":"repo_page","content_markdown":"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](codegen-workflow).\n\nFor the overall pipeline shape (why `api-core` exists, how `sdk-node` and `cli-node` bundle it), see [Architecture: Shared Codegen Pipeline](codegen-architecture). For the OpenAPI 3.1 → 3.0.3 normalization rules applied here, see [OpenAPI Spec Normalization and Codegen Artifacts](openapi-spec-normalization).\n\n## generate-openapi-artifacts.ts\n\nReads `openapi/primitive-api.yaml` (the authored OpenAPI 3.1 source of truth) and writes three files. Run it directly with:\n\n```bash\ncd packages/api-core\npnpm generate:openapi\n```\n\nInternally this is `tsx scripts/generate-openapi-artifacts.ts`, declared in `packages/api-core/package.json`.\n\n### Inputs and outputs\n\n| Path | Role |\n|---|---|\n| `openapi/primitive-api.yaml` | Input. Parsed with the `yaml` package. |\n| `openapi/primitive-api.codegen.json` | Output. Normalized OpenAPI 3.0.3 JSON for the code generators. |\n| `packages/api-core/src/openapi/openapi.generated.ts` | Output. Exports `openapiDocument`, the raw (non-normalized) spec as a TypeScript constant. |\n| `packages/api-core/src/openapi/operations.generated.ts` | Output. Exports `operationManifest`, `PrimitiveOperationManifest`, and `PrimitiveParameterManifest`. |\n\n### What normalization does\n\nBefore writing the codegen JSON, the script runs `normalizeForCodegen` over a deep clone of the raw spec:\n\n- **Nullable-type unwrapping**: an OpenAPI 3.1 `type: [\"string\", \"null\"]` array becomes `type: \"string\"` plus `nullable: true`, since @hey-api/openapi-ts's 3.0.3-oriented generator does not consume the 3.1 array form.\n- **Content-type remapping for binary bodies**: `message/rfc822` and `application/gzip` request/response content are rekeyed to `application/octet-stream` so the generator treats them as binary payloads.\n- The result is stamped `openapi: \"3.0.3\"` and written to `primitive-api.codegen.json`.\n\nThe 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](operation-manifest) infrastructure serve as the literal OpenAPI document, distinct from the codegen-only JSON.\n\n<Note>\n\nNever 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](openapi-spec-normalization).\n\n</Note>\n\n### Building the operation manifest\n\nFor 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`:\n\n| Field | Description |\n|---|---|\n| `command` | kebab-case form of `operationId` (e.g. `sendEmail` → `send-email`). |\n| `operationId` / `sdkName` | The original OpenAPI `operationId`. |\n| `method` | Uppercased HTTP method. |\n| `path` | The OpenAPI path template. |\n| `tag` / `tagCommand` | First declared tag, and its kebab-case form. |\n| `summary` / `description` | Copied from the operation, or `null`. |\n| `pathParams` / `queryParams` | Resolved parameters (merging path-item-level and operation-level parameters, `$ref`s included), split by `in: path` / `in: query`. Each includes `name`, `type`, `required`, `description`, `enum`, `default`, `minimum`, `maximum`. |\n| `bodyRequired` | `true` when `requestBody.required` is set. |\n| `hasJsonBody` | `true` when `requestBody.content[\"application/json\"].schema` exists. |\n| `requestSchema` | The request body JSON Schema with every `$ref` inlined, or `null`. |\n| `responseSchema` | The JSON Schema for the `data` property of the 200/201 response envelope, `$ref`s inlined, or `null` if no 200/201 JSON response exists. |\n| `binaryResponse` | `true` when any response declares `application/octet-stream`, `application/gzip`, `message/rfc822` content, or a `format: binary` schema. |\n\nThe manifest is sorted by `tagCommand`, then `command`.\n\n<Accordion title=\"How requestSchema and responseSchema are resolved\">\n\nBoth 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.\n\n`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.\n\nThis is what powers `primitive list-operations | jq '.[] | select(.command == \"send-email\") | .requestSchema'` and the CLI's `describe` command; see [Operation Manifest Reference](operation-manifest) and [Direct API Access and Generic Commands](cli-generic-api-access).\n\n</Accordion>\n\n### When to re-run it\n\nRe-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.\n\n## fix-generated-api-imports.ts\n\n`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`:\n\n```bash\ncd packages/api-core\npnpm generate:api\n```\n\nThat 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.\n\n### 1. Add `.js` extensions to relative imports\n\n`@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 `../`:\n\n- Leaves `.js` / `.json` specifiers untouched.\n- If the specifier resolves to a directory containing `index.ts`, rewrites it to `<specifier>/index.js`.\n- Otherwise rewrites it to `<specifier>.js`.\n\n### 2. Guard optional-body `Content-Type` headers\n\n`@hey-api/openapi-ts` unconditionally emits:\n\n```ts\nheaders: {\n  'Content-Type': 'application/json',\n  ...options.headers\n}\n```\n\non 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:\n\n```ts\nheaders: {\n  ...(options.body !== undefined && { 'Content-Type': 'application/json' }),\n  ...options.headers\n}\n```\n\nso 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](non-typescript-codegen) and [Codegen Troubleshooting](codegen-troubleshooting).\n\n### 3. Repair the `MemoryJsonValue` type\n\n`@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:\n\n```ts\nexport type MemoryJsonValue = string | number | boolean | Array<MemoryJsonValue> | {\n    [key: string]: MemoryJsonValue;\n} | null;\n```\n\nIf 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](memory-json-value-helper) for the runtime counterpart, `isMemoryJsonValue`.\n\n### When to re-run it\n\nNever 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.\n\n## Running both together\n\n```bash\ncd packages/api-core\npnpm generate\n```\n\n`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](codegen-workflow).","canonical_base_url":"https://test.abhinandan.one","seo_indexing_enabled":true,"last_modified":"2026-08-21T18:22:43.359885+00:00","video_url":null,"voiceover_url":null,"tools_used":[],"demonstrated_by":[],"steps":[],"related_links":[],"intro":null,"prerequisites":[],"verification":[],"troubleshooting":[],"suggest_edit_url":"https://github.com/abhi-browzer/primitive-sdks/edit/main/packages/api-core/scripts/generate-openapi-artifacts.ts","raise_issue_url":"https://github.com/abhi-browzer/primitive-sdks/issues/new?title=Docs+feedback%3A+Codegen+Artifact+Generation+Scripts&body=Page%3A+https%3A%2F%2Ftest.abhinandan.one%2Fapi-core-generation-scripts","page_feedback_enabled":true,"verified_ref":null,"verified_at":"2026-08-11T18:38:45.205849+00:00"}}