diff --git a/packages/server/src/router-utils.test.ts b/packages/server/src/router-utils.test.ts index 591ea6a65..6ce4ea36f 100644 --- a/packages/server/src/router-utils.test.ts +++ b/packages/server/src/router-utils.test.ts @@ -176,6 +176,36 @@ describe('traverseContractProcedures', () => { path: ['nested', 'pong'], }) }) + + it('with callable procedures', () => { + const callablePing = Object.assign(() => {}, { + '~orpc': ping['~orpc'], + }) + + const callablePong = Object.assign(() => {}, { + '~orpc': pong['~orpc'], + }) + + const router = { ping: callablePing, nested: { pong: callablePong } } + + const callback = vi.fn() + expect(traverseContractProcedures({ + router, + path: [], + }, callback)).toEqual([]) + + expect(callback).toHaveBeenCalledTimes(2) + + expect(callback).toHaveBeenNthCalledWith(1, { + contract: router.ping, + path: ['ping'], + }) + + expect(callback).toHaveBeenNthCalledWith(2, { + contract: router.nested.pong, + path: ['nested', 'pong'], + }) + }) }) it('resolveContractProcedures', async () => { diff --git a/packages/server/src/router-utils.ts b/packages/server/src/router-utils.ts index a2b02a547..a39f87522 100644 --- a/packages/server/src/router-utils.ts +++ b/packages/server/src/router-utils.ts @@ -5,6 +5,7 @@ import type { AnyMiddleware } from './middleware' import type { AnyProcedure } from './procedure' import type { AnyRouter } from './router' import { enhanceRoute, isContractProcedure, mergeErrorMap, mergePrefix } from '@orpc/contract' +import { isTypescriptObject } from '@orpc/shared' import { getLazyMeta, isLazy, lazy, unlazy } from './lazy' import { mergeMiddlewares } from './middleware-utils' import { isProcedure, Procedure } from './procedure' @@ -27,7 +28,7 @@ export function getRouter>( return undefined as any } - if (typeof current !== 'object') { + if (!isTypescriptObject(current)) { return undefined as any } @@ -192,16 +193,11 @@ export function traverseContractProcedures( callback: (options: TraverseContractProcedureCallbackOptions) => void, lazyOptions: LazyTraverseContractProceduresOptions[] = [], ): LazyTraverseContractProceduresOptions[] { - // Guard before reading the hidden-contract symbol so that null/undefined - // child exports don't crash in `getHiddenRouterContract`. Primitives like - // strings autobox safely; only null/undefined throw on symbol access. - if (typeof options.router !== 'object' || options.router === null) { - return lazyOptions - } - let currentRouter: AnyContractRouter | Lazyable = options.router - const hiddenContract = getHiddenRouterContract(options.router) + const hiddenContract = isTypescriptObject(options.router) + ? getHiddenRouterContract(options.router) + : undefined if (hiddenContract !== undefined) { currentRouter = hiddenContract