Skip to content

Commit

Permalink
Zod esm fix (#8)
Browse files Browse the repository at this point in the history
  • Loading branch information
mmkal authored Jun 2, 2024
1 parent 8a36dd8 commit a3140da
Show file tree
Hide file tree
Showing 5 changed files with 39 additions and 17 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ The above can be invoked with either `yarn` or `yarn install`.
### API docs

<!-- codegen:start {preset: markdownFromJsdoc, source: src/index.ts, export: trpcCli} -->
#### [trpcCli](./src/index.ts#L29)
#### [trpcCli](./src/index.ts#L30)

Run a trpc router as a CLI.

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "trpc-cli",
"version": "0.3.0",
"version": "0.3.1-0",
"description": "Turn a tRPC router into a type-safe, fully-functional, documented CLI",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
5 changes: 3 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import * as zodValidationError from 'zod-validation-error'
import {flattenedProperties, incompatiblePropertyPairs, getDescription} from './json-schema'
import {lineByLineConsoleLogger} from './logging'
import {Logger, TrpcCliParams} from './types'
import {looksLikeInstanceof} from './util'
import {parseProcedureInputs} from './zod-procedure'

export * from './types'
Expand Down Expand Up @@ -181,9 +182,9 @@ export const trpcCli = <R extends AnyRouter>({router, ...params}: TrpcCliParams<
type Fail = (message: string, options?: {cause?: unknown; help?: boolean}) => never

function transformError(err: unknown, fail: Fail): unknown {
if (err instanceof TRPCError) {
if (looksLikeInstanceof(err, TRPCError)) {
const cause = err.cause
if (cause instanceof ZodError) {
if (looksLikeInstanceof(cause, ZodError)) {
const originalIssues = cause.issues
try {
cause.issues = cause.issues.map(issue => {
Expand Down
17 changes: 17 additions & 0 deletions src/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* Pretty much like the `instanceof` operator, but should work across different realms. Necessary for zod because some installations
* might result in this library using the commonjs zod export, while the user's code uses the esm export.
* https://github.com/mmkal/trpc-cli/issues/7
*
* Tradeoff: It's possible that this function will return false positives if the target class has the same name as an unrelated class in the current realm.
* So, only use it for classes that are unlikely to have name conflicts like `ZodAbc` or `TRPCDef`.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const looksLikeInstanceof = <T>(value: unknown, target: new (...args: any[]) => T): value is T => {
let current = value?.constructor
do {
if (current?.name === target.name) return true
current = Object.getPrototypeOf(current) as Function
} while (current?.name)
return false
}
30 changes: 17 additions & 13 deletions src/zod-procedure.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import {z} from 'zod'
import zodToJsonSchema from 'zod-to-json-schema'
import type {Result, ParsedProcedure} from './types'
import {looksLikeInstanceof} from './util'

function getInnerType(zodType: z.ZodType): z.ZodType {
if (zodType instanceof z.ZodOptional || zodType instanceof z.ZodNullable) {
if (looksLikeInstanceof(zodType, z.ZodOptional) || looksLikeInstanceof(zodType, z.ZodNullable)) {
return getInnerType(zodType._def.innerType as z.ZodType)
}
if (zodType instanceof z.ZodEffects) {
if (looksLikeInstanceof(zodType, z.ZodEffects)) {
return getInnerType(zodType.innerType() as z.ZodType)
}
return zodType
Expand All @@ -20,7 +21,9 @@ export function parseProcedureInputs(inputs: unknown[]): Result<ParsedProcedure>
}
}

const allZodTypes = inputs.every(input => input instanceof z.ZodType)
const allZodTypes = inputs.every(input =>
looksLikeInstanceof(input, z.ZodType as new (...args: unknown[]) => z.ZodType),
)
if (!allZodTypes) {
return {
success: false,
Expand All @@ -38,8 +41,8 @@ export function parseProcedureInputs(inputs: unknown[]): Result<ParsedProcedure>
return parseLiteralInput(mergedSchema)
}

if (mergedSchema instanceof z.ZodTuple) {
return parseTupleInput(mergedSchema as z.ZodTuple<never>)
if (looksLikeInstanceof<z.ZodTuple<never>>(mergedSchema, z.ZodTuple)) {
return parseTupleInput(mergedSchema)
}

if (!acceptsObject(mergedSchema)) {
Expand Down Expand Up @@ -222,16 +225,17 @@ const parameterName = (s: z.ZodType, position: number) => {
* acceptsString(z.intersection(z.string(), z.number())) // false
* acceptsString(z.intersection(z.string(), z.string().max(10))) // true
*/
export function accepts(target: z.ZodType) {
export function accepts<ZodTarget extends z.ZodType>(target: ZodTarget) {
const test = (zodType: z.ZodType): boolean => {
const innerType = getInnerType(zodType)
if (innerType instanceof target.constructor) return true
if (innerType instanceof z.ZodLiteral) return target.safeParse(innerType.value).success
if (innerType instanceof z.ZodEnum) return (innerType.options as unknown[]).some(o => target.safeParse(o).success)
if (innerType instanceof z.ZodUnion) return (innerType.options as z.ZodType[]).some(test)
if (innerType instanceof z.ZodIntersection)
return test(innerType._def.left as z.ZodType) && test(innerType._def.right as z.ZodType)
if (innerType instanceof z.ZodEffects) return test(innerType.innerType() as z.ZodType)
if (looksLikeInstanceof(innerType, target.constructor as new (...args: unknown[]) => ZodTarget)) return true
if (looksLikeInstanceof(innerType, z.ZodLiteral)) return target.safeParse(innerType.value).success
if (looksLikeInstanceof(innerType, z.ZodEnum)) return innerType.options.some(o => target.safeParse(o).success)
if (looksLikeInstanceof(innerType, z.ZodUnion)) return innerType.options.some(test)
if (looksLikeInstanceof<z.ZodEffects<z.ZodType>>(innerType, z.ZodEffects)) return test(innerType.innerType())
if (looksLikeInstanceof<z.ZodIntersection<z.ZodType, z.ZodType>>(innerType, z.ZodIntersection))
return test(innerType._def.left) && test(innerType._def.right)

return false
}
return test
Expand Down

0 comments on commit a3140da

Please sign in to comment.