Skip to content

Commit 9d8bcc0

Browse files
committed
fix: ignore Zod.describe method to not interfere with type generation
1 parent 9be9143 commit 9d8bcc0

6 files changed

Lines changed: 142 additions & 23 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Unreleased
44

5+
## 0.0.10 - 2025-11-24
6+
7+
- fix: ignore Zod.describe method to not interfere with type generation
8+
59
## 0.0.9 - 2025-11-20
610

711
- feat: upgrade to zod v4 + support for `meta`

README.md

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ $ npm install -g @reforge-com/cli
1414
$ reforge COMMAND
1515
running command...
1616
$ reforge (--version)
17-
@reforge-com/cli/0.0.9 darwin-arm64 node-v24.4.1
17+
@reforge-com/cli/0.0.10 darwin-arm64 node-v24.4.1
1818
$ reforge --help [COMMAND]
1919
USAGE
2020
$ reforge COMMAND
@@ -91,7 +91,7 @@ EXAMPLES
9191
$ reforge create my.new.string --type json --value="{\"key\": \"value\"}"
9292
```
9393

94-
_See code: [src/commands/create.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.9/src/commands/create.ts)_
94+
_See code: [src/commands/create.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.10/src/commands/create.ts)_
9595

9696
## `reforge download`
9797

@@ -124,7 +124,7 @@ EXAMPLES
124124
$ reforge download --environment=test --sdk-key=YOUR_SDK_KEY
125125
```
126126

127-
_See code: [src/commands/download.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.9/src/commands/download.ts)_
127+
_See code: [src/commands/download.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.10/src/commands/download.ts)_
128128

129129
## `reforge generate`
130130

@@ -194,7 +194,7 @@ EXAMPLES
194194
$ reforge generate --targets node-ts -o ./dist # combine with targets
195195
```
196196
197-
_See code: [src/commands/generate.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.9/src/commands/generate.ts)_
197+
_See code: [src/commands/generate.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.10/src/commands/generate.ts)_
198198
199199
## `reforge generate-new-hex-key`
200200
@@ -217,7 +217,7 @@ EXAMPLES
217217
$ reforge generate-new-hex-key
218218
```
219219
220-
_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)_
220+
_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)_
221221
222222
## `reforge get [NAME]`
223223
@@ -250,7 +250,7 @@ EXAMPLES
250250
$ reforge get my.config.name --environment=production
251251
```
252252
253-
_See code: [src/commands/get.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.9/src/commands/get.ts)_
253+
_See code: [src/commands/get.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.10/src/commands/get.ts)_
254254
255255
## `reforge info [NAME]`
256256
@@ -281,7 +281,7 @@ EXAMPLES
281281
$ reforge info my.config.name
282282
```
283283
284-
_See code: [src/commands/info.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.9/src/commands/info.ts)_
284+
_See code: [src/commands/info.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.10/src/commands/info.ts)_
285285
286286
## `reforge interactive`
287287
@@ -299,7 +299,7 @@ EXAMPLES
299299
$ reforge
300300
```
301301
302-
_See code: [src/commands/interactive.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.9/src/commands/interactive.ts)_
302+
_See code: [src/commands/interactive.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.10/src/commands/interactive.ts)_
303303
304304
## `reforge list`
305305
@@ -336,7 +336,7 @@ EXAMPLES
336336
$ reforge list --feature-flags
337337
```
338338
339-
_See code: [src/commands/list.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.9/src/commands/list.ts)_
339+
_See code: [src/commands/list.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.10/src/commands/list.ts)_
340340
341341
## `reforge login`
342342
@@ -364,7 +364,7 @@ EXAMPLES
364364
$ reforge login --profile myprofile
365365
```
366366
367-
_See code: [src/commands/login.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.9/src/commands/login.ts)_
367+
_See code: [src/commands/login.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.10/src/commands/login.ts)_
368368
369369
## `reforge logout`
370370
@@ -387,7 +387,7 @@ EXAMPLES
387387
$ reforge logout
388388
```
389389
390-
_See code: [src/commands/logout.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.9/src/commands/logout.ts)_
390+
_See code: [src/commands/logout.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.10/src/commands/logout.ts)_
391391
392392
## `reforge mcp`
393393
@@ -420,7 +420,7 @@ EXAMPLES
420420
$ reforge mcp --url http://local-launch.goatsofreforge.com:3003/api/v1/mcp
421421
```
422422
423-
_See code: [src/commands/mcp.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.9/src/commands/mcp.ts)_
423+
_See code: [src/commands/mcp.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.10/src/commands/mcp.ts)_
424424
425425
## `reforge override [NAME]`
426426
@@ -459,7 +459,7 @@ EXAMPLES
459459
$ reforge override my.double.config --value=3.14159
460460
```
461461
462-
_See code: [src/commands/override.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.9/src/commands/override.ts)_
462+
_See code: [src/commands/override.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.10/src/commands/override.ts)_
463463
464464
## `reforge profile`
465465
@@ -482,7 +482,7 @@ EXAMPLES
482482
$ reforge profile
483483
```
484484
485-
_See code: [src/commands/profile.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.9/src/commands/profile.ts)_
485+
_See code: [src/commands/profile.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.10/src/commands/profile.ts)_
486486
487487
## `reforge schema NAME`
488488
@@ -516,7 +516,7 @@ EXAMPLES
516516
$ reforge schema my-schema --get
517517
```
518518
519-
_See code: [src/commands/schema.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.9/src/commands/schema.ts)_
519+
_See code: [src/commands/schema.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.10/src/commands/schema.ts)_
520520
521521
## `reforge serve DATA-FILE`
522522
@@ -552,7 +552,7 @@ EXAMPLES
552552
$ reforge serve ./reforge.test.588.config.json --port=3099
553553
```
554554
555-
_See code: [src/commands/serve.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.9/src/commands/serve.ts)_
555+
_See code: [src/commands/serve.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.10/src/commands/serve.ts)_
556556
557557
## `reforge set-default [NAME]`
558558
@@ -596,7 +596,7 @@ EXAMPLES
596596
$ reforge set-default my.config.name --env-var=MY_ENV_VAR_NAME --environment=production
597597
```
598598
599-
_See code: [src/commands/set-default.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.9/src/commands/set-default.ts)_
599+
_See code: [src/commands/set-default.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.10/src/commands/set-default.ts)_
600600
601601
## `reforge whoami`
602602
@@ -619,7 +619,7 @@ EXAMPLES
619619
$ reforge whoami
620620
```
621621
622-
_See code: [src/commands/whoami.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.9/src/commands/whoami.ts)_
622+
_See code: [src/commands/whoami.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.10/src/commands/whoami.ts)_
623623
624624
## `reforge workspace`
625625
@@ -642,7 +642,7 @@ EXAMPLES
642642
$ reforge workspace
643643
```
644644
645-
_See code: [src/commands/workspace.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.9/src/commands/workspace.ts)_
645+
_See code: [src/commands/workspace.ts](https://github.com/ReforgeHQ/cli/blob/v0.0.10/src/commands/workspace.ts)_
646646
<!-- commandsstop -->
647647
648648
## Local Development

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"packageManager": "yarn@4.11.0",
33
"name": "@reforge-com/cli",
4-
"version": "0.0.9",
4+
"version": "0.0.10",
55
"author": "Jeffrey Chupp @semanticart",
66
"bugs": {
77
"url": "https://github.com/ReforgeHQ/cli/issues"

src/codegen/schema-evaluator.ts

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,45 @@ export function validateAst(ast: any, parentMap: WeakMap<any, any>): {error?: st
203203
return {error, isValid}
204204
}
205205

206+
/**
207+
* Removes .describe() calls from the AST by filtering them out
208+
* This is more robust than regex as it properly handles nested structures
209+
*
210+
* @param ast - The AST to process
211+
* @param schemaString - The original schema string
212+
* @returns The schema string with .describe() calls removed
213+
*/
214+
function removeDescribeCalls(ast: any, schemaString: string): string {
215+
const nodesToRemove: Array<{start: number; end: number}> = []
216+
217+
// Walk the AST and find all .describe() calls
218+
walk.simple(ast, {
219+
CallExpression(node: any) {
220+
if (
221+
node.callee.type === 'MemberExpression' &&
222+
node.callee.property.type === 'Identifier' &&
223+
node.callee.property.name === 'describe'
224+
) {
225+
// Mark the entire .describe(...) call for removal
226+
// We want to remove from the dot before 'describe' to the closing paren
227+
const start = node.callee.property.start - 1 // Include the dot
228+
const end = node.end
229+
nodesToRemove.push({start, end})
230+
}
231+
},
232+
})
233+
234+
// Sort in reverse order so we can remove from the end without affecting earlier positions
235+
nodesToRemove.sort((a, b) => b.start - a.start)
236+
237+
let result = schemaString
238+
for (const {start, end} of nodesToRemove) {
239+
result = result.slice(0, Math.max(0, start)) + result.slice(Math.max(0, end))
240+
}
241+
242+
return result
243+
}
244+
206245
/**
207246
* Securely evaluates a Zod schema string using AST validation
208247
*
@@ -265,11 +304,29 @@ export function secureEvaluateSchema(
265304
if (!isValid) {
266305
return {error, success: false}
267306
}
307+
308+
// Remove .describe() calls after validation
309+
const filteredSchema = removeDescribeCalls(ast, trimmedSchema)
310+
311+
// Phase 2: Execute with Function constructor
312+
// We're deliberately avoiding VM modules due to security concerns
313+
// The AST validation provides our primary security layer
314+
// eslint-disable-next-line no-new-func
315+
const constructSchema = new Function('z', `return ${filteredSchema}`)
316+
const schema = constructSchema(z)
317+
318+
// Phase 3: Validate Result
319+
if (!schema || typeof schema !== 'object' || !('_def' in schema)) {
320+
return {
321+
error: 'The provided string did not evaluate to a valid Zod schema',
322+
success: false,
323+
}
324+
}
325+
326+
return {schema, success: true}
268327
}
269328

270-
// Phase 2: Execute with Function constructor
271-
// We're deliberately avoiding VM modules due to security concerns
272-
// The AST validation provides our primary security layer
329+
// If AST validation is disabled, just execute directly (not recommended)
273330
// eslint-disable-next-line no-new-func
274331
const constructSchema = new Function('z', `return ${trimmedSchema}`)
275332
const schema = constructSchema(z)

test/codegen/language-mappers/zod-to-typescript-mapper.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,5 +413,29 @@ describe('ZodToTypescriptMapper', () => {
413413
)
414414
})
415415
})
416+
417+
describe('ignores describe', () => {
418+
it('Can successfully parse a string with describe (value is ignored)', () => {
419+
const zodAst = secureEvaluateSchema(`z.string().describe("User's email address")`)
420+
421+
const mapper = new ZodToTypescriptMapper({fieldName: 'email'})
422+
423+
const rendered = mapper.renderField(zodAst.schema!)
424+
425+
expect(rendered).to.equal('"email": string')
426+
})
427+
428+
it('Can successfully parse a string with meta + describe (value is ignored)', () => {
429+
const zodAst = secureEvaluateSchema(
430+
`z.string().meta({description: "Meta description"}).describe("Description description")`,
431+
)
432+
433+
const mapper = new ZodToTypescriptMapper({fieldName: 'email'})
434+
435+
const rendered = mapper.renderField(zodAst.schema!)
436+
437+
expect(rendered).to.equal('/** Meta description */ "email": string')
438+
})
439+
})
416440
})
417441
})

test/codegen/schema-evaluator.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,5 +120,39 @@ describe('SchemaEvaluator', () => {
120120
expect(introspect.isBoolean(options[2])).to.be.true
121121
}
122122
})
123+
124+
it('should strip .describe() calls from schema', () => {
125+
const result = secureEvaluateSchema('z.string().describe("A string field")')
126+
127+
expect(result.success).to.be.true
128+
expect(result.schema).to.exist
129+
130+
// Verify the schema is a plain string schema without description
131+
if (result.schema) {
132+
expect(introspect.isString(result.schema)).to.be.true
133+
}
134+
})
135+
136+
it('should strip .describe() and keep .meta()', () => {
137+
const result = secureEvaluateSchema('z.string().describe("desc").meta({ description: "A string field" })')
138+
139+
expect(result.success).to.be.true
140+
expect(result.schema).to.exist
141+
142+
// Verify meta is still accessible
143+
if (result.schema) {
144+
expect(introspect.isString(result.schema)).to.be.true
145+
const meta = introspect.getMeta(result.schema)
146+
expect(meta).to.exist
147+
expect(meta?.description).to.equal('A string field')
148+
}
149+
})
150+
151+
it('should allow .meta() with description', () => {
152+
const result = secureEvaluateSchema('z.string().meta({ description: "A string field" })')
153+
154+
expect(result.success).to.be.true
155+
expect(result.schema).to.exist
156+
})
123157
})
124158
})

0 commit comments

Comments
 (0)