diff --git a/src/index.ts b/src/index.ts index bcf1d4b..560c4b4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,7 @@ import {type JsonSchema7Type} from 'zod-to-json-schema' import * as zodValidationError from 'zod-validation-error' import {flattenedProperties, incompatiblePropertyPairs, getDescription} from './json-schema' import {lineByLineConsoleLogger} from './logging' +import {looksLikeInstanceof} from './uitl' import {Logger, TrpcCliParams} from './types' import {parseProcedureInputs} from './zod-procedure' @@ -181,9 +182,9 @@ export const trpcCli = ({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 => { diff --git a/src/uitl.ts b/src/uitl.ts new file mode 100644 index 0000000..672c65e --- /dev/null +++ b/src/uitl.ts @@ -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 = (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 +} diff --git a/src/zod-procedure.ts b/src/zod-procedure.ts index 01ea20a..ddcc62c 100644 --- a/src/zod-procedure.ts +++ b/src/zod-procedure.ts @@ -1,12 +1,13 @@ import {z} from 'zod' import zodToJsonSchema from 'zod-to-json-schema' import type {Result, ParsedProcedure} from './types' +import {looksLikeInstanceof} from './uitl' 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 @@ -20,7 +21,9 @@ export function parseProcedureInputs(inputs: unknown[]): Result } } - 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, @@ -38,7 +41,7 @@ export function parseProcedureInputs(inputs: unknown[]): Result return parseLiteralInput(mergedSchema) } - if (mergedSchema instanceof z.ZodTuple) { + if (looksLikeInstanceof(mergedSchema, z.ZodTuple)) { return parseTupleInput(mergedSchema as z.ZodTuple) } @@ -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(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) + 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 as unknown[]).some(o => target.safeParse(o).success) + if (looksLikeInstanceof(innerType, z.ZodUnion)) return innerType.options.some(test) + if (looksLikeInstanceof(innerType, 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, z.ZodEffects)) return test(innerType.innerType() as z.ZodType) return false } return test