Generated TypeScript Client Fixups
A post-processing script repairs three specific defects in the @hey-api/openapi-ts generated output before api-core ships it: missing.js import extensions, an unconditional optional-body Content-Type header, and a widened MemoryJsonValue type.
What the fixup script does#
fix-generated-api-imports.ts, the post-processing script in packages/api-core/scripts, applies three repairs to the TypeScript client that @hey-api/openapi-ts generates into packages/api-core/src/api. It walks every generated .ts file and rewrites what the generator gets wrong. It runs as the second half of api-core's generate:api script (openapi-ts -f openapi-ts.config.ts && tsx scripts/fix-generated-api-imports.ts), so it always follows codegen and is never run standalone. See Regenerating SDK Code from the OpenAPI Spec for the full pipeline this fits into.
None of these are spec authoring changes. They exist because the generator's output needs correcting before api-core is bundled into @primitivedotdev/sdk and primitive.
Fix 1: add .js extensions to relative imports#
The script appends .js (or /index.js for directories) to every relative import specifier, because @hey-api/openapi-ts emits them without an extension. api-core is a pure ESM package, and Node's ESM resolver requires explicit extensions on relative specifiers; an extensionless import fails at runtime with ERR_MODULE_NOT_FOUND.
The script rewrites every relative import ... from and dynamic import(...) specifier:
// packages/api-core/scripts/fix-generated-api-imports.ts
function addJsExtension(file: string, specifier: string): string {
if (!specifier.startsWith("./") && !specifier.startsWith("../")) {
return specifier;
}
if (specifier.endsWith(".js") || specifier.endsWith(".json")) {
return specifier;
}
const absolute = resolve(dirname(file), specifier);
if (existsSync(absolute) && statSync(absolute).isDirectory() && existsSync(join(absolute, "index.ts"))) {
return `${specifier}/index.js`;
}
if (existsSync(`${absolute}.ts`)) {
return `${specifier}.js`;
}
return `${specifier}.js`;
}
It resolves each specifier against the source file's directory to detect whether it points at a directory (needs /index.js) or a file (.js suffix), leaving absolute imports (bare package names) untouched.
Fix 2: guard the optional-body Content-Type header#
The script makes the generated Content-Type: application/json header conditional on a body actually being present. @hey-api/openapi-ts emits headers: { 'Content-Type': 'application/json', ...options.headers } on every operation whose OpenAPI spec declares a request body, regardless of whether that body is required or optional. For optional-body operations (the script names testFunction, cli_logout, start_cli_login, and search_emails), that sends the header on the wire even when the caller omits the body, which is wrong: a request with no body should carry no Content-Type.
guardOptionalBodyContentType rewrites the header assignment so it only fires when options.body is actually defined:
// packages/api-core/scripts/fix-generated-api-imports.ts
function guardOptionalBodyContentType(content: string): string {
return content.replace(
/(headers:\s*\{\n\s*)'Content-Type':\s*'application\/json',\n(\s*\.\.\.options\.headers\n\s*\})/g,
"$1...(options.body !== undefined && { 'Content-Type': 'application/json' }),\n$2",
);
}
Required-body operations are unaffected: the type system guarantees body is always defined for those, so the header still ships in practice.
The Python generator produces the same defect, and sdk-python/scripts/generate_api_client.py applies an equivalent repair in guard_optional_body_content_type. See Non-TypeScript Codegen: Go and Python Clients.
Fix 3: repair the MemoryJsonValue type#
The script replaces the generated MemoryJsonValue alias with the exact recursive JSON type. The spec's MemoryJsonValue schema is a recursive JSON-value union including a type: "null" branch (OpenAPI 3.1 syntax), and @hey-api/openapi-ts currently treats that branch as unknown, widening the whole alias.
fixMemoryJsonValueType substitutes the correct recursive type:
// packages/api-core/scripts/fix-generated-api-imports.ts
const MEMORY_JSON_VALUE_TYPE = `export type MemoryJsonValue = string | number | boolean | Array<MemoryJsonValue> | {
[key: string]: MemoryJsonValue;
} | null;`;
It matches the generated block up to the doc comment for the next declaration (Memory scope.) and substitutes the corrected type in place. If the substitution doesn't find or produce the expected string, the script throws immediately rather than silently shipping a broken type:
// packages/api-core/scripts/fix-generated-api-imports.ts
if (updated === content || !updated.includes(MEMORY_JSON_VALUE_TYPE)) {
throw new Error(
`Unable to repair generated MemoryJsonValue type in ${file}. The codegen output shape changed; update fixMemoryJsonValueType before publishing.`,
);
}
That hard failure is intentional: a codegen version bump that reshapes the output is exactly the kind of change this repair is fragile against, and a loud build break beats a silently-reintroduced unknown leaking into @primitivedotdev/sdk's public types.
Where this fits in the pipeline#
The fixup pass is stage 4 of five, between TypeScript codegen and bundling.
| Stage | What runs | Output |
|---|---|---|
| 1. Spec authoring | Hand-edit openapi/primitive-api.yaml | OpenAPI 3.1 source of truth |
| 2. Normalization | generate-openapi-artifacts.ts | primitive-api.codegen.json (3.0.3), operation manifest |
| 3. TS codegen | openapi-ts (@hey-api/openapi-ts) | Raw generated client in packages/api-core/src/api |
| 4. This fixup pass | fix-generated-api-imports.ts | Corrected .js imports, guarded headers, fixed MemoryJsonValue |
| 5. Bundle | sdk-node / cli-node inline api-core | Published packages |
For how stage 2 produces the normalized spec these fixups build on top of, see OpenAPI Spec Normalization and Codegen Artifacts. For the scripts that produce the manifest and embedded OpenAPI document alongside this one, see Codegen Artifact Generation Scripts.
Next steps#
Run the full pipeline this fixup script is one step of.
Codegen Artifact Generation ScriptsSee the companion script that produces the operation manifest and embedded OpenAPI document.
Non-TypeScript Codegen: Go and Python ClientsCompare the equivalent post-processing fixups applied to the Go and Python generators.
Codegen TroubleshootingDiagnose failures when this fixup's assumptions about generated output drift.
Was this page helpful?