chore: bump to 0.2.2 + make Zod schemas version-agnostic (v3.25+/v4) (#178)

* feat: make generated Zod schemas version-agnostic (v3.25+/v4)

- Change generated schema imports from `zod/v4` to standard `zod`
- Replace v4-only `z.looseObject()` with `z.object().passthrough()`
- Works with both Zod v3.25+ and v4, matching MCP TS SDK approach
- Bump version to 0.2.2

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* chore: regenerate package-lock.json in Docker

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Olivier Chafik 2025-12-17 20:55:59 +00:00 • committed by GitHub
commit 82ab3c894c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -8,12 +8,10 @@
*
* ts-to-zod is a powerful tool but has limitations that require post-processing:
*
* ### 1. Zod Import Path (`"zod"` → `"zod/v4"`)
* ### 1. Zod Import Path (keep standard `"zod"` for version agnosticism)
*
* **Problem**: ts-to-zod generates `import { z } from "zod"` but this project
* uses the Zod v4 subpath import `"zod/v4"` for explicit version targeting.
*
* **Solution**: Replace the import path in the generated output.
* ts-to-zod generates `import { z } from "zod"` which works with both
* Zod v3.25+ and v4. We keep this standard import to support both versions.
*
* ### 2. External Type References (`z.any()` → actual schemas)
*
@ -24,13 +22,14 @@
*
* **Solution**: Import the schemas from MCP SDK and remove the z.any() placeholders.
*
* ### 3. Index Signatures (`z.record().and()` → `z.looseObject()`)
* ### 3. Index Signatures (`z.record().and()` → `z.object().passthrough()`)
*
* **Problem**: TypeScript index signatures like `[key: string]: unknown` are
* translated by ts-to-zod to `z.record(z.string(), z.unknown()).and(z.object({...}))`.
* This creates a `ZodIntersection` type which doesn't support `.extend()` etc.
*
* **Solution**: Replace the intersection pattern with `z.looseObject()`.
* **Solution**: Replace with `z.object({...}).passthrough()` which works in both
* Zod v3 and v4, allowing extra properties while validating known ones.
*
* ## Adding Schema Descriptions
*
@ -178,23 +177,17 @@ async function generateJsonSchema() {
* Post-process generated schemas for project compatibility.
*/
function postProcess(content: string): string {
// 1. Update import to use zod/v4
content = content.replace(
'import { z } from "zod";',
'import * as z from "zod/v4";',
);
// 2. Add MCP SDK schema imports
// 1. Add MCP SDK schema imports (keep standard zod import for v3/v4 compatibility)
const mcpImports = EXTERNAL_TYPE_SCHEMAS.join(",\n ");
content = content.replace(
'import * as z from "zod/v4";',
`import * as z from "zod/v4";
'import { z } from "zod";',
`import { z } from "zod";
import {
${mcpImports},
} from "@modelcontextprotocol/sdk/types.js";`,
);
// 3. Remove z.any() placeholders for external types (now imported from MCP SDK)
// 2. Remove z.any() placeholders for external types (now imported from MCP SDK)
for (const schema of EXTERNAL_TYPE_SCHEMAS) {
content = content.replace(
new RegExp(`(?:export )?const ${schema} = z\\.any\\(\\);\\n?`, "g"),
@ -202,15 +195,16 @@ import {
);
}
// 4. Replace z.record().and(z.object({...})) with z.looseObject({...})
// 3. Replace z.record().and(z.object({...})) with z.object({...}).passthrough()
// Uses brace-counting to handle nested objects correctly.
content = replaceRecordAndWithLooseObject(content);
// passthrough() works in both Zod v3 and v4, unlike looseObject() which is v4-only.
content = replaceRecordAndWithPassthrough(content);
// 5. Add header comment
// 4. Add header comment
content = content.replace(
"// Generated by ts-to-zod",
`// Generated by ts-to-zod
// Post-processed for Zod v4 and MCP SDK compatibility
// Post-processed for Zod v3/v4 compatibility and MCP SDK integration
// Run: npm run generate:schemas`,
);
@ -218,10 +212,11 @@ import {
}
/**
* Replace z.record(z.string(), z.unknown()).and(z.object({...})) with z.looseObject({...})
* Replace z.record(z.string(), z.unknown()).and(z.object({...})) with z.object({...}).passthrough()
* Uses brace-counting to handle nested objects correctly.
* passthrough() works in both Zod v3 and v4, allowing extra properties.
*/
function replaceRecordAndWithLooseObject(content: string): string {
function replaceRecordAndWithPassthrough(content: string): string {
const pattern = "z.record(z.string(), z.unknown()).and(z.object({";
let result = content;
let startIndex = 0;
@ -245,7 +240,7 @@ function replaceRecordAndWithLooseObject(content: string): string {
// Check if followed by ))
if (result.slice(i, i + 2) === "))") {
const objectContent = result.slice(objectStart, i - 1);
const replacement = `z.looseObject({${objectContent}})`;
const replacement = `z.object({${objectContent}}).passthrough()`;
result = result.slice(0, matchStart) + replacement + result.slice(i + 2);
startIndex = matchStart + replacement.length;
} else {
@ -260,11 +255,7 @@ function replaceRecordAndWithLooseObject(content: string): string {
* Post-process generated integration tests.
*/
function postProcessTests(content: string): string {
content = content.replace(
'import { z } from "zod";',
'import * as z from "zod/v4";',
);
// Keep standard zod import for v3/v4 compatibility
content = content.replace(
"// Generated by ts-to-zod",
`// Generated by ts-to-zod