From 9d8bcc04a7f1c95283f0ce0e18d4ae0759e77a5f Mon Sep 17 00:00:00 2001 From: Mark Faga Date: Mon, 24 Nov 2025 12:24:32 -0500 Subject: [PATCH] fix: ignore Zod.describe method to not interfere with type generation --- CHANGELOG.md | 4 ++ README.md | 38 +++++------ package.json | 2 +- src/codegen/schema-evaluator.ts | 63 ++++++++++++++++++- .../zod-to-typescript-mapper.test.ts | 24 +++++++ test/codegen/schema-evaluator.test.ts | 34 ++++++++++ 6 files changed, 142 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index da6ecb1..9840149 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## 0.0.10 - 2025-11-24 + +- fix: ignore Zod.describe method to not interfere with type generation + ## 0.0.9 - 2025-11-20 - feat: upgrade to zod v4 + support for `meta` diff --git a/README.md b/README.md index 6ab99bf..9550478 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ $ npm install -g @reforge-com/cli $ reforge COMMAND running command... $ reforge (--version) -@reforge-com/cli/0.0.9 darwin-arm64 node-v24.4.1 +@reforge-com/cli/0.0.10 darwin-arm64 node-v24.4.1 $ reforge --help [COMMAND] USAGE $ reforge COMMAND @@ -91,7 +91,7 @@ EXAMPLES $ reforge create my.new.string --type json --value="{\"key\": \"value\"}" ``` -_See code: [src/commands/create.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.9/src/commands/create.ts)_ +_See code: [src/commands/create.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.10/src/commands/create.ts)_ ## `reforge download` @@ -124,7 +124,7 @@ EXAMPLES $ reforge download --environment=test --sdk-key=YOUR_SDK_KEY ``` -_See code: [src/commands/download.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.9/src/commands/download.ts)_ +_See code: [src/commands/download.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.10/src/commands/download.ts)_ ## `reforge generate` @@ -194,7 +194,7 @@ EXAMPLES $ reforge generate --targets node-ts -o ./dist # combine with targets ``` -_See code: [src/commands/generate.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.9/src/commands/generate.ts)_ +_See code: [src/commands/generate.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.10/src/commands/generate.ts)_ ## `reforge generate-new-hex-key` @@ -217,7 +217,7 @@ EXAMPLES $ reforge generate-new-hex-key ``` -_See code: [src/commands/generate-new-hex-key.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.9/src/commands/generate-new-hex-key.ts)_ +_See code: [src/commands/generate-new-hex-key.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.10/src/commands/generate-new-hex-key.ts)_ ## `reforge get [NAME]` @@ -250,7 +250,7 @@ EXAMPLES $ reforge get my.config.name --environment=production ``` -_See code: [src/commands/get.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.9/src/commands/get.ts)_ +_See code: [src/commands/get.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.10/src/commands/get.ts)_ ## `reforge info [NAME]` @@ -281,7 +281,7 @@ EXAMPLES $ reforge info my.config.name ``` -_See code: [src/commands/info.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.9/src/commands/info.ts)_ +_See code: [src/commands/info.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.10/src/commands/info.ts)_ ## `reforge interactive` @@ -299,7 +299,7 @@ EXAMPLES $ reforge ``` -_See code: [src/commands/interactive.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.9/src/commands/interactive.ts)_ +_See code: [src/commands/interactive.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.10/src/commands/interactive.ts)_ ## `reforge list` @@ -336,7 +336,7 @@ EXAMPLES $ reforge list --feature-flags ``` -_See code: [src/commands/list.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.9/src/commands/list.ts)_ +_See code: [src/commands/list.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.10/src/commands/list.ts)_ ## `reforge login` @@ -364,7 +364,7 @@ EXAMPLES $ reforge login --profile myprofile ``` -_See code: [src/commands/login.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.9/src/commands/login.ts)_ +_See code: [src/commands/login.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.10/src/commands/login.ts)_ ## `reforge logout` @@ -387,7 +387,7 @@ EXAMPLES $ reforge logout ``` -_See code: [src/commands/logout.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.9/src/commands/logout.ts)_ +_See code: [src/commands/logout.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.10/src/commands/logout.ts)_ ## `reforge mcp` @@ -420,7 +420,7 @@ EXAMPLES $ reforge mcp --url http://local-launch.goatsofreforge.com:3003/api/v1/mcp ``` -_See code: [src/commands/mcp.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.9/src/commands/mcp.ts)_ +_See code: [src/commands/mcp.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.10/src/commands/mcp.ts)_ ## `reforge override [NAME]` @@ -459,7 +459,7 @@ EXAMPLES $ reforge override my.double.config --value=3.14159 ``` -_See code: [src/commands/override.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.9/src/commands/override.ts)_ +_See code: [src/commands/override.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.10/src/commands/override.ts)_ ## `reforge profile` @@ -482,7 +482,7 @@ EXAMPLES $ reforge profile ``` -_See code: [src/commands/profile.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.9/src/commands/profile.ts)_ +_See code: [src/commands/profile.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.10/src/commands/profile.ts)_ ## `reforge schema NAME` @@ -516,7 +516,7 @@ EXAMPLES $ reforge schema my-schema --get ``` -_See code: [src/commands/schema.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.9/src/commands/schema.ts)_ +_See code: [src/commands/schema.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.10/src/commands/schema.ts)_ ## `reforge serve DATA-FILE` @@ -552,7 +552,7 @@ EXAMPLES $ reforge serve ./reforge.test.588.config.json --port=3099 ``` -_See code: [src/commands/serve.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.9/src/commands/serve.ts)_ +_See code: [src/commands/serve.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.10/src/commands/serve.ts)_ ## `reforge set-default [NAME]` @@ -596,7 +596,7 @@ EXAMPLES $ reforge set-default my.config.name --env-var=MY_ENV_VAR_NAME --environment=production ``` -_See code: [src/commands/set-default.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.9/src/commands/set-default.ts)_ +_See code: [src/commands/set-default.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.10/src/commands/set-default.ts)_ ## `reforge whoami` @@ -619,7 +619,7 @@ EXAMPLES $ reforge whoami ``` -_See code: [src/commands/whoami.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.9/src/commands/whoami.ts)_ +_See code: [src/commands/whoami.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.10/src/commands/whoami.ts)_ ## `reforge workspace` @@ -642,7 +642,7 @@ EXAMPLES $ reforge workspace ``` -_See code: [src/commands/workspace.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.9/src/commands/workspace.ts)_ +_See code: [src/commands/workspace.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.10/src/commands/workspace.ts)_ ## Local Development diff --git a/package.json b/package.json index c3ff0d2..60d4859 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "packageManager": "yarn@4.11.0", "name": "@reforge-com/cli", - "version": "0.0.9", + "version": "0.0.10", "author": "Jeffrey Chupp @semanticart", "bugs": { "url": "https://github.com/ReforgeHQ/cli/issues" diff --git a/src/codegen/schema-evaluator.ts b/src/codegen/schema-evaluator.ts index f7d1b7d..7aad4ed 100644 --- a/src/codegen/schema-evaluator.ts +++ b/src/codegen/schema-evaluator.ts @@ -203,6 +203,45 @@ export function validateAst(ast: any, parentMap: WeakMap): {error?: st return {error, isValid} } +/** + * Removes .describe() calls from the AST by filtering them out + * This is more robust than regex as it properly handles nested structures + * + * @param ast - The AST to process + * @param schemaString - The original schema string + * @returns The schema string with .describe() calls removed + */ +function removeDescribeCalls(ast: any, schemaString: string): string { + const nodesToRemove: Array<{start: number; end: number}> = [] + + // Walk the AST and find all .describe() calls + walk.simple(ast, { + CallExpression(node: any) { + if ( + node.callee.type === 'MemberExpression' && + node.callee.property.type === 'Identifier' && + node.callee.property.name === 'describe' + ) { + // Mark the entire .describe(...) call for removal + // We want to remove from the dot before 'describe' to the closing paren + const start = node.callee.property.start - 1 // Include the dot + const end = node.end + nodesToRemove.push({start, end}) + } + }, + }) + + // Sort in reverse order so we can remove from the end without affecting earlier positions + nodesToRemove.sort((a, b) => b.start - a.start) + + let result = schemaString + for (const {start, end} of nodesToRemove) { + result = result.slice(0, Math.max(0, start)) + result.slice(Math.max(0, end)) + } + + return result +} + /** * Securely evaluates a Zod schema string using AST validation * @@ -265,11 +304,29 @@ export function secureEvaluateSchema( if (!isValid) { return {error, success: false} } + + // Remove .describe() calls after validation + const filteredSchema = removeDescribeCalls(ast, trimmedSchema) + + // Phase 2: Execute with Function constructor + // We're deliberately avoiding VM modules due to security concerns + // The AST validation provides our primary security layer + // eslint-disable-next-line no-new-func + const constructSchema = new Function('z', `return ${filteredSchema}`) + const schema = constructSchema(z) + + // Phase 3: Validate Result + if (!schema || typeof schema !== 'object' || !('_def' in schema)) { + return { + error: 'The provided string did not evaluate to a valid Zod schema', + success: false, + } + } + + return {schema, success: true} } - // Phase 2: Execute with Function constructor - // We're deliberately avoiding VM modules due to security concerns - // The AST validation provides our primary security layer + // If AST validation is disabled, just execute directly (not recommended) // eslint-disable-next-line no-new-func const constructSchema = new Function('z', `return ${trimmedSchema}`) const schema = constructSchema(z) diff --git a/test/codegen/language-mappers/zod-to-typescript-mapper.test.ts b/test/codegen/language-mappers/zod-to-typescript-mapper.test.ts index 523fe15..1b0b1e4 100644 --- a/test/codegen/language-mappers/zod-to-typescript-mapper.test.ts +++ b/test/codegen/language-mappers/zod-to-typescript-mapper.test.ts @@ -413,5 +413,29 @@ describe('ZodToTypescriptMapper', () => { ) }) }) + + describe('ignores describe', () => { + it('Can successfully parse a string with describe (value is ignored)', () => { + const zodAst = secureEvaluateSchema(`z.string().describe("User's email address")`) + + const mapper = new ZodToTypescriptMapper({fieldName: 'email'}) + + const rendered = mapper.renderField(zodAst.schema!) + + expect(rendered).to.equal('"email": string') + }) + + it('Can successfully parse a string with meta + describe (value is ignored)', () => { + const zodAst = secureEvaluateSchema( + `z.string().meta({description: "Meta description"}).describe("Description description")`, + ) + + const mapper = new ZodToTypescriptMapper({fieldName: 'email'}) + + const rendered = mapper.renderField(zodAst.schema!) + + expect(rendered).to.equal('/** Meta description */ "email": string') + }) + }) }) }) diff --git a/test/codegen/schema-evaluator.test.ts b/test/codegen/schema-evaluator.test.ts index 9831ba9..0023b32 100644 --- a/test/codegen/schema-evaluator.test.ts +++ b/test/codegen/schema-evaluator.test.ts @@ -120,5 +120,39 @@ describe('SchemaEvaluator', () => { expect(introspect.isBoolean(options[2])).to.be.true } }) + + it('should strip .describe() calls from schema', () => { + const result = secureEvaluateSchema('z.string().describe("A string field")') + + expect(result.success).to.be.true + expect(result.schema).to.exist + + // Verify the schema is a plain string schema without description + if (result.schema) { + expect(introspect.isString(result.schema)).to.be.true + } + }) + + it('should strip .describe() and keep .meta()', () => { + const result = secureEvaluateSchema('z.string().describe("desc").meta({ description: "A string field" })') + + expect(result.success).to.be.true + expect(result.schema).to.exist + + // Verify meta is still accessible + if (result.schema) { + expect(introspect.isString(result.schema)).to.be.true + const meta = introspect.getMeta(result.schema) + expect(meta).to.exist + expect(meta?.description).to.equal('A string field') + } + }) + + it('should allow .meta() with description', () => { + const result = secureEvaluateSchema('z.string().meta({ description: "A string field" })') + + expect(result.success).to.be.true + expect(result.schema).to.exist + }) }) })