From 7eacbb9b3a7ff22f87979d37a13ca765f80cda42 Mon Sep 17 00:00:00 2001 From: unnoq Date: Thu, 26 Jun 2025 16:44:39 +0700 Subject: [PATCH 1/7] init --- packages/trpc/.gitignore | 26 +++++++++++++ packages/trpc/README.md | 76 +++++++++++++++++++++++++++++++++++++ packages/trpc/package.json | 45 ++++++++++++++++++++++ packages/trpc/src/index.ts | 0 packages/trpc/tsconfig.json | 16 ++++++++ pnpm-lock.yaml | 16 ++++++++ 6 files changed, 179 insertions(+) create mode 100644 packages/trpc/.gitignore create mode 100644 packages/trpc/README.md create mode 100644 packages/trpc/package.json create mode 100644 packages/trpc/src/index.ts create mode 100644 packages/trpc/tsconfig.json diff --git a/packages/trpc/.gitignore b/packages/trpc/.gitignore new file mode 100644 index 000000000..f3620b55e --- /dev/null +++ b/packages/trpc/.gitignore @@ -0,0 +1,26 @@ +# Hidden folders and files +.* +!.gitignore +!.*.example + +# Common generated folders +logs/ +node_modules/ +out/ +dist/ +dist-ssr/ +build/ +coverage/ +temp/ + +# Common generated files +*.log +*.log.* +*.tsbuildinfo +*.vitest-temp.json +vite.config.ts.timestamp-* +vitest.config.ts.timestamp-* + +# Common manual ignore files +*.local +*.pem \ No newline at end of file diff --git a/packages/trpc/README.md b/packages/trpc/README.md new file mode 100644 index 000000000..df99e95cb --- /dev/null +++ b/packages/trpc/README.md @@ -0,0 +1,76 @@ +
+ oRPC logo +
+ +

+ +
+ + codecov + + + weekly downloads + + + MIT License + + + Discord + +
+ +

Typesafe APIs Made Simple 🪄

+ +**oRPC is a powerful combination of RPC and OpenAPI**, makes it easy to build APIs that are end-to-end type-safe and adhere to OpenAPI standards + +--- + +## Highlights + +- **🔗 End-to-End Type Safety**: Ensure type-safe inputs, outputs, and errors from client to server. +- **📘 First-Class OpenAPI**: Built-in support that fully adheres to the OpenAPI standard. +- **📝 Contract-First Development**: Optionally define your API contract before implementation. +- **⚙️ Framework Integrations**: Seamlessly integrate with TanStack Query (React, Vue, Solid, Svelte, Angular), Pinia Colada, and more. +- **🚀 Server Actions**: Fully compatible with React Server Actions on Next.js, TanStack Start, and other platforms. +- **🔠 Standard Schema Support**: Works out of the box with Zod, Valibot, ArkType, and other schema validators. +- **🗃️ Native Types**: Supports native types like Date, File, Blob, BigInt, URL, and more. +- **⏱️ Lazy Router**: Enhance cold start times with our lazy routing feature. +- **📡 SSE & Streaming**: Enjoy full type-safe support for SSE and streaming. +- **🌍 Multi-Runtime Support**: Fast and lightweight on Cloudflare, Deno, Bun, Node.js, and beyond. +- **🔌 Extendability**: Easily extend functionality with plugins, middleware, and interceptors. +- **🛡️ Reliability**: Well-tested, TypeScript-based, production-ready, and MIT licensed. + +## Documentation + +You can find the full documentation [here](https://orpc.unnoq.com). + +## Packages + +- [@orpc/contract](https://www.npmjs.com/package/@orpc/contract): Build your API contract. +- [@orpc/server](https://www.npmjs.com/package/@orpc/server): Build your API or implement API contract. +- [@orpc/client](https://www.npmjs.com/package/@orpc/client): Consume your API on the client with type-safety. +- [@orpc/openapi](https://www.npmjs.com/package/@orpc/openapi): Generate OpenAPI specs and handle OpenAPI requests. +- [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Deeply integrate oRPC with [NestJS](https://nestjs.com/). +- [@orpc/react](https://www.npmjs.com/package/@orpc/react): Utilities for integrating oRPC with React and React Server Actions. +- [@orpc/tanstack-query](https://www.npmjs.com/package/@orpc/tanstack-query): [TanStack Query](https://tanstack.com/query/latest) integration. +- [@orpc/vue-colada](https://www.npmjs.com/package/@orpc/vue-colada): Integration with [Pinia Colada](https://pinia-colada.esm.dev/). +- [@orpc/hey-api](https://www.npmjs.com/package/@orpc/hey-api): [Hey API](https://heyapi.dev/) integration. +- [@orpc/zod](https://www.npmjs.com/package/@orpc/zod): More schemas that [Zod](https://zod.dev/) doesn't support yet. +- [@orpc/valibot](https://www.npmjs.com/package/@orpc/valibot): OpenAPI spec generation from [Valibot](https://valibot.dev/). +- [@orpc/arktype](https://www.npmjs.com/package/@orpc/arktype): OpenAPI spec generation from [ArkType](https://arktype.io/). + +## `@orpc/trpc` + +Bridge between [tRPC](https://trpc.io/) and oRPC + +## Sponsors + +

+ + + +

+ +## License + +Distributed under the MIT License. See [LICENSE](https://github.com/unnoq/orpc/blob/main/LICENSE) for more information. diff --git a/packages/trpc/package.json b/packages/trpc/package.json new file mode 100644 index 000000000..64038c41e --- /dev/null +++ b/packages/trpc/package.json @@ -0,0 +1,45 @@ +{ + "name": "@orpc/trpc", + "type": "module", + "version": "1.5.2", + "license": "MIT", + "homepage": "https://orpc.unnoq.com", + "repository": { + "type": "git", + "url": "git+https://github.com/unnoq/orpc.git", + "directory": "packages/trpc" + }, + "keywords": [ + "unnoq", + "orpc", + "trpc" + ], + "publishConfig": { + "exports": { + ".": { + "types": "./dist/index.d.mts", + "import": "./dist/index.mjs", + "default": "./dist/index.mjs" + } + } + }, + "exports": { + ".": "./src/index.ts" + }, + "files": [ + "dist" + ], + "scripts": { + "build": "unbuild", + "build:watch": "pnpm run build --watch", + "type:check": "tsc -b" + }, + "dependencies": { + "@orpc/contract": "workspace:*", + "@orpc/server": "workspace:*", + "@orpc/shared": "workspace:*" + }, + "devDependencies": { + "zod": "^3.25.67" + } +} diff --git a/packages/trpc/src/index.ts b/packages/trpc/src/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/trpc/tsconfig.json b/packages/trpc/tsconfig.json new file mode 100644 index 000000000..e29b5e150 --- /dev/null +++ b/packages/trpc/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.lib.json", + "references": [ + { "path": "../contract" }, + { "path": "../server" }, + { "path": "../shared" } + ], + "include": ["src"], + "exclude": [ + "**/*.test.*", + "**/*.test-d.ts", + "**/__tests__/**", + "**/__mocks__/**", + "**/__snapshots__/**" + ] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d58a59c05..3b9ef63f3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -656,6 +656,22 @@ importers: specifier: ^3.25.67 version: 3.25.67 + packages/trpc: + dependencies: + '@orpc/contract': + specifier: workspace:* + version: link:../contract + '@orpc/server': + specifier: workspace:* + version: link:../server + '@orpc/shared': + specifier: workspace:* + version: link:../shared + devDependencies: + zod: + specifier: ^3.25.67 + version: 3.25.67 + packages/valibot: dependencies: '@orpc/contract': From 3722717d6166551591fd09044f2e7851325505ab Mon Sep 17 00:00:00 2001 From: unnoq Date: Thu, 26 Jun 2025 20:57:02 +0700 Subject: [PATCH 2/7] wip --- packages/trpc/package.json | 5 ++- packages/trpc/src/to-orpc-router.test-d.ts | 38 ++++++++++++++++++ packages/trpc/src/to-orpc-router.ts | 31 +++++++++++++++ packages/trpc/tests/shared.ts | 45 ++++++++++++++++++++++ packages/trpc/tsconfig.json | 1 - pnpm-lock.yaml | 15 ++++++-- 6 files changed, 130 insertions(+), 5 deletions(-) create mode 100644 packages/trpc/src/to-orpc-router.test-d.ts create mode 100644 packages/trpc/src/to-orpc-router.ts create mode 100644 packages/trpc/tests/shared.ts diff --git a/packages/trpc/package.json b/packages/trpc/package.json index 64038c41e..ebbb716ea 100644 --- a/packages/trpc/package.json +++ b/packages/trpc/package.json @@ -34,12 +34,15 @@ "build:watch": "pnpm run build --watch", "type:check": "tsc -b" }, + "peerDependencies": { + "@trpc/server": ">=11.4.2" + }, "dependencies": { - "@orpc/contract": "workspace:*", "@orpc/server": "workspace:*", "@orpc/shared": "workspace:*" }, "devDependencies": { + "@trpc/server": "^11.4.2", "zod": "^3.25.67" } } diff --git a/packages/trpc/src/to-orpc-router.test-d.ts b/packages/trpc/src/to-orpc-router.test-d.ts new file mode 100644 index 000000000..d748aa727 --- /dev/null +++ b/packages/trpc/src/to-orpc-router.test-d.ts @@ -0,0 +1,38 @@ +import type { InferRouterInitialContext, Lazyable, Procedure, Schema, Router, ContractRouter } from '@orpc/server' +import type { ToORPCRouterResult } from './to-orpc-router' +import { inferRouterContext } from '@trpc/server' +import { inferRouterMeta } from '@trpc/server/unstable-core-do-not-import' +import { TRPCContext, TRPCMeta, trpcRouter } from '../tests/shared' + +it('ToORPCRouterResult', () => { + const orpcRouter = {} as ToORPCRouterResult< + inferRouterContext, + inferRouterMeta, + typeof trpcRouter['_def']['record'] + > + + expectTypeOf(orpcRouter).toExtend, TRPCContext>>() + + expectTypeOf>().toEqualTypeOf<{ a: string }>() + + expectTypeOf(orpcRouter.ping).toEqualTypeOf< + Lazyable, Schema, object, TRPCMeta>> + >() + + expectTypeOf(orpcRouter.pong).toEqualTypeOf< + Lazyable, Schema, object, TRPCMeta>> + >() + + expectTypeOf(orpcRouter.subscribe).toEqualTypeOf< + Lazyable, Schema>, object, TRPCMeta>> + >() + + expectTypeOf(orpcRouter.nested).toEqualTypeOf< + Lazyable<{ + subscribe: Lazyable < Procedure, Schema>, object, TRPCMeta>> + nested: Lazyable<{ + pong: Lazyable, Schema, object, TRPCMeta>> + }> + }> + >() +}) diff --git a/packages/trpc/src/to-orpc-router.ts b/packages/trpc/src/to-orpc-router.ts new file mode 100644 index 000000000..bccad4dc6 --- /dev/null +++ b/packages/trpc/src/to-orpc-router.ts @@ -0,0 +1,31 @@ +import type * as ORPC from '@orpc/server' +import type { AnyProcedure, AnyRouter, inferRouterContext } from '@trpc/server' +import type { inferRouterMeta, Procedure, Router } from '@trpc/server/unstable-core-do-not-import' + +export type ToORPCRouterResult> + = { + [K in keyof TRecord]: ORPC.Lazyable< + TRecord[K] extends AnyProcedure + ? ORPC.Procedure< + TContext, + object, + ORPC.Schema, + ORPC.Schema, + object, + TMeta + > + : TRecord[K] extends Record + ? ToORPCRouterResult + : never + > + } + +export function experimental_toORPCRouter( + router: T +): ToORPCRouterResult< + inferRouterContext, + inferRouterMeta, + T['_def']['record'] +> { + return {} as any +} diff --git a/packages/trpc/tests/shared.ts b/packages/trpc/tests/shared.ts new file mode 100644 index 000000000..f0c2dee69 --- /dev/null +++ b/packages/trpc/tests/shared.ts @@ -0,0 +1,45 @@ +import { initTRPC, lazy } from "@trpc/server" +import { z } from "zod/v4" + +export type TRPCContext = { a: string } +export type TRPCMeta = { meta1?: string, meta2?: number } +export const t = initTRPC.context<(req: Request) => (TRPCContext)>().meta().create() + +export const trpcRouter = t.router({ + ping: t.procedure + .meta({ meta1: 'test' }) + .input(z.object({ a: z.string() })) + .output(z.string().transform(val => Number(val))) + .query(({ input }) => { + return `1234${input.a}` + }), + + pong: t.procedure + .meta({ meta2: 42 }) + .input(z.object({ b: z.number() })) + .input(z.object({ b: z.number(), c: z.string() })) + .query(({ input }) => { + return `ping ${input.b}` + }), + + subscribe: t.procedure + .input(z.object({ u: z.string() })) + .subscription(async function* () { + yield 'pong' + }), + + nested: lazy(() => Promise.resolve({ default: t.router({ + subscribe: t.procedure + .subscription(async function* () { + yield 'pong' + }), + + nested: lazy(() => Promise.resolve({ default: t.router({ + pong: t.procedure + .meta({ meta1: 'nested' }) + .input(z.object({ d: z.boolean() })) + .output(z.string()) + .query(() => 'nested nested pong'), + }) })), + }) })), + }) diff --git a/packages/trpc/tsconfig.json b/packages/trpc/tsconfig.json index e29b5e150..0d99cefed 100644 --- a/packages/trpc/tsconfig.json +++ b/packages/trpc/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../tsconfig.lib.json", "references": [ - { "path": "../contract" }, { "path": "../server" }, { "path": "../shared" } ], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3b9ef63f3..04560c1ed 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -658,9 +658,6 @@ importers: packages/trpc: dependencies: - '@orpc/contract': - specifier: workspace:* - version: link:../contract '@orpc/server': specifier: workspace:* version: link:../server @@ -668,6 +665,9 @@ importers: specifier: workspace:* version: link:../shared devDependencies: + '@trpc/server': + specifier: ^11.4.2 + version: 11.4.2(typescript@5.8.3) zod: specifier: ^3.25.67 version: 3.25.67 @@ -4492,6 +4492,11 @@ packages: resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} engines: {node: '>= 10'} + '@trpc/server@11.4.2': + resolution: {integrity: sha512-THyq/V5bSFDHeWEAk6LqHF0IVTGk6voGwWsFEipzRRKOWWMIZINCsKZ4cISG6kWO2X9jBfMWv/S2o9hnC0zQ0w==} + peerDependencies: + typescript: '>=5.7.2' + '@trysound/sax@0.2.0': resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} engines: {node: '>=10.13.0'} @@ -15985,6 +15990,10 @@ snapshots: '@tootallnate/once@2.0.0': {} + '@trpc/server@11.4.2(typescript@5.8.3)': + dependencies: + typescript: 5.8.3 + '@trysound/sax@0.2.0': {} '@ts-rest/core@3.52.1(@types/node@24.0.3)(zod@3.25.67)': From 287ddbaf1fa6ea121de79851cb2c5d83abde2424 Mon Sep 17 00:00:00 2001 From: unnoq Date: Fri, 27 Jun 2025 20:45:13 +0700 Subject: [PATCH 3/7] wip --- packages/trpc/src/to-orpc-router.test-d.ts | 42 +++--- packages/trpc/src/to-orpc-router.test.ts | 76 +++++++++++ packages/trpc/src/to-orpc-router.ts | 148 +++++++++++++++++---- packages/trpc/tests/shared.ts | 72 ++++++---- 4 files changed, 271 insertions(+), 67 deletions(-) create mode 100644 packages/trpc/src/to-orpc-router.test.ts diff --git a/packages/trpc/src/to-orpc-router.test-d.ts b/packages/trpc/src/to-orpc-router.test-d.ts index d748aa727..e237af9de 100644 --- a/packages/trpc/src/to-orpc-router.test-d.ts +++ b/packages/trpc/src/to-orpc-router.test-d.ts @@ -1,13 +1,13 @@ -import type { InferRouterInitialContext, Lazyable, Procedure, Schema, Router, ContractRouter } from '@orpc/server' -import type { ToORPCRouterResult } from './to-orpc-router' -import { inferRouterContext } from '@trpc/server' -import { inferRouterMeta } from '@trpc/server/unstable-core-do-not-import' -import { TRPCContext, TRPCMeta, trpcRouter } from '../tests/shared' +import type { ContractRouter, InferRouterInitialContext, Procedure, Router, Schema } from '@orpc/server' +import type { inferRouterContext } from '@trpc/server' +import type { inferRouterMeta } from '@trpc/server/unstable-core-do-not-import' +import type { TRPCContext, TRPCMeta, trpcRouter } from '../tests/shared' +import type { experimental_ToORPCRouterResult as ToORPCRouterResult } from './to-orpc-router' it('ToORPCRouterResult', () => { const orpcRouter = {} as ToORPCRouterResult< inferRouterContext, - inferRouterMeta, + inferRouterMeta, typeof trpcRouter['_def']['record'] > @@ -16,23 +16,29 @@ it('ToORPCRouterResult', () => { expectTypeOf>().toEqualTypeOf<{ a: string }>() expectTypeOf(orpcRouter.ping).toEqualTypeOf< - Lazyable, Schema, object, TRPCMeta>> + Procedure, Schema, object, TRPCMeta> >() - expectTypeOf(orpcRouter.pong).toEqualTypeOf< - Lazyable, Schema, object, TRPCMeta>> + expectTypeOf(orpcRouter.throw).toEqualTypeOf< + Procedure, Schema, object, TRPCMeta> >() - expectTypeOf(orpcRouter.subscribe).toEqualTypeOf< - Lazyable, Schema>, object, TRPCMeta>> - >() + expectTypeOf(orpcRouter.subscribe).toEqualTypeOf< + Procedure, Schema>, object, TRPCMeta> + >() expectTypeOf(orpcRouter.nested).toEqualTypeOf< - Lazyable<{ - subscribe: Lazyable < Procedure, Schema>, object, TRPCMeta>> - nested: Lazyable<{ - pong: Lazyable, Schema, object, TRPCMeta>> - }> - }> + { + ping: Procedure, Schema, object, TRPCMeta> + } + >() + + expectTypeOf(orpcRouter.lazy).toEqualTypeOf< + { + subscribe: Procedure, Schema>, object, TRPCMeta> + lazy: { + throw: Procedure, Schema, object, TRPCMeta> + } + } >() }) diff --git a/packages/trpc/src/to-orpc-router.test.ts b/packages/trpc/src/to-orpc-router.test.ts new file mode 100644 index 000000000..2816a01c4 --- /dev/null +++ b/packages/trpc/src/to-orpc-router.test.ts @@ -0,0 +1,76 @@ +import { call, createRouterClient, isProcedure, ORPCError, unlazy } from '@orpc/server' +import { isAsyncIteratorObject } from '@orpc/shared' +import { inputSchema, outputSchema } from '../../contract/tests/shared' +import { trpcRouter } from '../tests/shared' +import { experimental_toORPCRouter as toORPCRouter } from './to-orpc-router' + +describe('toORPCRouter', async () => { + const orpcRouter = toORPCRouter(trpcRouter) + + it('shape', async () => { + expect(orpcRouter.ping).toSatisfy(isProcedure) + expect(orpcRouter.throw).toSatisfy(isProcedure) + expect(orpcRouter.nested.ping).toSatisfy(isProcedure) + + const unlazy1 = await unlazy(orpcRouter.lazy) + expect(unlazy1.default.subscribe).toSatisfy(isProcedure) + + const unlazy2 = await unlazy(unlazy1.default.lazy) + + expect(unlazy2.default.throw).toSatisfy(isProcedure) + }) + + it('with disabled input/output', async () => { + expect((orpcRouter as any).ping['~orpc'].inputSchema['~standard'].vendor).toBe('zod') + expect((orpcRouter as any).ping['~orpc'].inputSchema._def).toBe(inputSchema._def) + expect((orpcRouter as any).ping['~orpc'].outputSchema['~standard'].vendor).toBe('zod') + expect((orpcRouter as any).ping['~orpc'].outputSchema._def).toBe(outputSchema._def) + + const invalidValue = 'INVALID' + expect((orpcRouter as any).ping['~orpc'].inputSchema['~standard'].validate(invalidValue)).toEqual({ value: invalidValue }) + expect((orpcRouter as any).ping['~orpc'].outputSchema['~standard'].validate(invalidValue)).toEqual({ value: invalidValue }) + }) + + it('meta/route', async () => { + expect(orpcRouter.ping['~orpc'].meta).toEqual({ meta1: 'test' }) + expect(orpcRouter.nested.ping['~orpc'].route).toEqual({ path: '/nested/ping', description: 'Nested ping procedure' }) + expect(orpcRouter.nested.ping['~orpc'].meta).toEqual({ path: '/nested/ping', description: 'Nested ping procedure' }) + }) + + describe('calls', () => { + it('on success', async () => { + const result = await call(orpcRouter.ping, { input: 1234 }, { context: { a: 'test' } }) + expect(result).toEqual({ output: '1234' }) + }) + + it('async iterator', async () => { + const result = await call(orpcRouter.subscribe, { u: 'u' }, { context: { a: 'test' } }) + expect(result).toSatisfy(isAsyncIteratorObject) + expect(await (result as any).next()).toEqual({ done: false, value: 'pong' }) + }) + + it('error', async () => { + await expect( + call(orpcRouter.throw, { b: 42, c: 'test' }, { context: { a: 'test' } }), + ).rejects.toSatisfy((err: any) => { + return err instanceof ORPCError && err.code === 'PARSE_ERROR' && err.message === 'throw' + }) + }) + + it('deep lazy', async () => { + const client = createRouterClient(orpcRouter, { + context: { a: 'test' }, + }) + + await expect( + client.lazy.subscribe(), + ).resolves.toSatisfy(isAsyncIteratorObject) + + await expect( + client.lazy.lazy.throw({ input: 1234 }), + ).rejects.toSatisfy((err: any) => { + return err instanceof ORPCError && err.message === 'lazy.lazy.throw' + }) + }) + }) +}) diff --git a/packages/trpc/src/to-orpc-router.ts b/packages/trpc/src/to-orpc-router.ts index bccad4dc6..ca75c2318 100644 --- a/packages/trpc/src/to-orpc-router.ts +++ b/packages/trpc/src/to-orpc-router.ts @@ -1,31 +1,133 @@ -import type * as ORPC from '@orpc/server' -import type { AnyProcedure, AnyRouter, inferRouterContext } from '@trpc/server' -import type { inferRouterMeta, Procedure, Router } from '@trpc/server/unstable-core-do-not-import' +import type { Parser } from '@trpc/server/unstable-core-do-not-import' +import * as ORPC from '@orpc/server' +import { isTypescriptObject } from '@orpc/shared' +import { type AnyProcedure, type AnyRouter, type inferRouterContext, TRPCError } from '@trpc/server' +import { getHTTPStatusCodeFromError, type inferRouterMeta } from '@trpc/server/unstable-core-do-not-import' -export type ToORPCRouterResult> +export interface experimental_ORPCMeta extends ORPC.Route { + +} + +export type experimental_ToORPCRouterResult> = { - [K in keyof TRecord]: ORPC.Lazyable< - TRecord[K] extends AnyProcedure - ? ORPC.Procedure< - TContext, - object, - ORPC.Schema, - ORPC.Schema, - object, - TMeta - > - : TRecord[K] extends Record - ? ToORPCRouterResult - : never - > - } + [K in keyof TRecord]: + TRecord[K] extends AnyProcedure + ? ORPC.Procedure< + TContext, + object, + ORPC.Schema, + ORPC.Schema, + object, + TMeta + > + : TRecord[K] extends Record + ? experimental_ToORPCRouterResult + : never + } export function experimental_toORPCRouter( - router: T -): ToORPCRouterResult< + router: T, +): experimental_ToORPCRouterResult< inferRouterContext, inferRouterMeta, T['_def']['record'] -> { - return {} as any + > { + const result = { + ...lazyToORPCRouter(router._def.lazy), + ...recordToORPCRouterRecord(router._def.record), + } + + return result as any +} + +function lazyToORPCRouter(lazies: AnyRouter['_def']['lazy']) { + const orpcRouter: Record = {} + + for (const key in lazies) { + const item = lazies[key]! + + orpcRouter[key] = ORPC.lazy(async () => { + const router = await item.ref() + return { default: experimental_toORPCRouter(router) } + }) + } + + return orpcRouter +} + +function recordToORPCRouterRecord(records: AnyRouter['_def']['record']) { + const orpcRouter: Record = {} + + for (const key in records) { + const item = records[key] + + if (typeof item === 'function') { + orpcRouter[key] = toORPCProcedure(item) + } + else { + orpcRouter[key] = recordToORPCRouterRecord(item) + } + } + + return orpcRouter +} + +function toORPCProcedure(procedure: AnyProcedure) { + return new ORPC.Procedure({ + errorMap: {}, + meta: procedure._def.meta ?? {}, + inputValidationIndex: 0, + outputValidationIndex: 0, + route: procedure._def.meta ?? {}, + middlewares: [], + inputSchema: toDisabledStandardSchema(procedure._def.inputs.at(-1)), + outputSchema: toDisabledStandardSchema((procedure as any)._def.output), + handler: async ({ context, signal, path, input }) => { + try { + return await procedure({ + ctx: context, + signal, + path: path.join('.'), + type: procedure._def.type, + input, + getRawInput: () => input, + }) + } + catch (cause) { + if (cause instanceof TRPCError) { + throw new ORPC.ORPCError(cause.code, { + status: getHTTPStatusCodeFromError(cause), + message: cause.message, + cause, + }) + } + + throw cause + } + }, + }) +} + +function toDisabledStandardSchema(schema: undefined | Parser): undefined | ORPC.Schema { + if (!isTypescriptObject(schema) || !('~standard' in schema) || !isTypescriptObject(schema['~standard'])) { + return undefined + } + + return new Proxy(schema as any, { + get: (target, prop) => { + if (prop === '~standard') { + return new Proxy(target['~standard'], { + get: (target, prop) => { + if (prop === 'validate') { + return (value: any) => ({ value }) + } + + return Reflect.get(target, prop, target) + }, + }) + } + + return Reflect.get(target, prop, target) + }, + }) } diff --git a/packages/trpc/tests/shared.ts b/packages/trpc/tests/shared.ts index f0c2dee69..13555cef2 100644 --- a/packages/trpc/tests/shared.ts +++ b/packages/trpc/tests/shared.ts @@ -1,45 +1,65 @@ -import { initTRPC, lazy } from "@trpc/server" -import { z } from "zod/v4" +import type { experimental_ORPCMeta as ORPCMeta } from '../src/to-orpc-router' +import { initTRPC, lazy, TRPCError } from '@trpc/server' +import { z } from 'zod/v4' +import { inputSchema, outputSchema } from '../../contract/tests/shared' export type TRPCContext = { a: string } -export type TRPCMeta = { meta1?: string, meta2?: number } +export interface TRPCMeta extends ORPCMeta { + meta1?: string + meta2?: number +} + export const t = initTRPC.context<(req: Request) => (TRPCContext)>().meta().create() export const trpcRouter = t.router({ + ping: t.procedure + .meta({ meta1: 'test' }) + .input(inputSchema) + .output(outputSchema) + .query(({ input }) => { + return { output: Number(input.input) } + }), + + throw: t.procedure + .meta({ meta2: 42 }) + .input(z.object({ b: z.number(), c: z.string() })) + .query(() => { + throw new TRPCError({ + code: 'PARSE_ERROR', + message: 'throw', + }) + }), + + subscribe: t.procedure + .input(z.object({ u: z.string() })) + .subscription(async function* () { + yield 'pong' + }), + + nested: { ping: t.procedure - .meta({ meta1: 'test' }) + .meta({ path: '/nested/ping', description: 'Nested ping procedure' }) .input(z.object({ a: z.string() })) .output(z.string().transform(val => Number(val))) .query(({ input }) => { return `1234${input.a}` }), + }, - pong: t.procedure - .meta({ meta2: 42 }) - .input(z.object({ b: z.number() })) - .input(z.object({ b: z.number(), c: z.string() })) - .query(({ input }) => { - return `ping ${input.b}` - }), - + lazy: lazy(() => Promise.resolve({ default: t.router({ subscribe: t.procedure - .input(z.object({ u: z.string() })) .subscription(async function* () { yield 'pong' }), - nested: lazy(() => Promise.resolve({ default: t.router({ - subscribe: t.procedure - .subscription(async function* () { - yield 'pong' + lazy: lazy(() => Promise.resolve({ default: t.router({ + throw: t.procedure + .meta({ meta1: 'nested' }) + .input(inputSchema) + .output(outputSchema) + .query(() => { + throw new Error('lazy.lazy.throw') }), - - nested: lazy(() => Promise.resolve({ default: t.router({ - pong: t.procedure - .meta({ meta1: 'nested' }) - .input(z.object({ d: z.boolean() })) - .output(z.string()) - .query(() => 'nested nested pong'), - }) })), }) })), - }) + }) })), +}) From fe2f22a734237b0142b15e1582fcf8edd6354da2 Mon Sep 17 00:00:00 2001 From: unnoq Date: Fri, 27 Jun 2025 21:20:18 +0700 Subject: [PATCH 4/7] docs --- apps/content/.vitepress/config.ts | 2 + .../content/docs/openapi/integrations/trpc.md | 124 ++++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 apps/content/docs/openapi/integrations/trpc.md diff --git a/apps/content/.vitepress/config.ts b/apps/content/.vitepress/config.ts index ca93d0623..c93946c22 100644 --- a/apps/content/.vitepress/config.ts +++ b/apps/content/.vitepress/config.ts @@ -158,6 +158,7 @@ export default defineConfig({ { text: 'Durable Event Iterator', link: '/docs/integrations/durable-event-iterator' }, { text: 'Hey API', link: '/docs/integrations/hey-api' }, { text: 'NestJS', link: '/docs/openapi/integrations/implement-contract-in-nest' }, + { text: 'tRPC', link: '/docs/openapi/integrations/trpc' }, ], }, { @@ -221,6 +222,7 @@ export default defineConfig({ collapsed: true, items: [ { text: 'Implement Contract in NestJS', link: '/docs/openapi/integrations/implement-contract-in-nest' }, + { text: 'tRPC', link: '/docs/openapi/integrations/trpc' }, ], }, { diff --git a/apps/content/docs/openapi/integrations/trpc.md b/apps/content/docs/openapi/integrations/trpc.md new file mode 100644 index 000000000..e1c0cf999 --- /dev/null +++ b/apps/content/docs/openapi/integrations/trpc.md @@ -0,0 +1,124 @@ +--- +title: tRPC Integration +description: Use oRPC features in your tRPC applications. +--- + +# tRPC Integration + +This guide explains how to integrate oRPC with tRPC, allowing you to leverage oRPC features in your existing tRPC applications. + +## Installation + +::: code-group + +```sh [npm] +npm install @orpc/trpc@latest +``` + +```sh [yarn] +yarn add @orpc/trpc@latest +``` + +```sh [pnpm] +pnpm add @orpc/trpc@latest +``` + +```sh [bun] +bun add @orpc/trpc@latest +``` + +```sh [deno] +deno install npm:@orpc/trpc@latest +``` + +::: + +## OpenAPI Support + +By converting a [tRPC router](https://trpc.io/docs/server/routers) to an [oRPC router](/docs/router), you can utilize most oRPC features, including OpenAPI specification generation and request handling. + +```ts +import { + experimental_ORPCMeta as ORPCMeta, + experimental_toORPCRouter as toORPCRouter +} from '@orpc/trpc' + +export const t = initTRPC.context().meta().create() + +const orpcRouter = toORPCRouter(trpcRouter) +``` + +::: warning +Ensure you set the `.meta` type to `ORPCMeta` when creating your tRPC builder. This is required for OpenAPI features to function properly. +::: + +### Specification Generation + +```ts +const openAPIGenerator = new OpenAPIGenerator({ + schemaConverters: [ + new ZodToJsonSchemaConverter(), // <-- if you use Zod + new ValibotToJsonSchemaConverter(), // <-- if you use Valibot + new ArkTypeToJsonSchemaConverter(), // <-- if you use ArkType + ], +}) + +const spec = await openAPIGenerator.generate(orpcRouter, { + info: { + title: 'My App', + version: '0.0.0', + }, +}) +``` + +::: info +Learn more about [oRPC OpenAPI Specification Generation](/docs/openapi/openapi-specification-generation). +::: + +### Request Handling + +```ts +const handler = new OpenAPIHandler(orpcRouter, { + plugins: [new CORSPlugin()], + interceptors: [ + onError(error => console.error(error)) + ], +}) + +export async function fetch(request: Request) { + const { matched, response } = await handler.handle(request, { + prefix: '/api', + context: {} // Add initial context if needed + }) + + return response ?? new Response('Not Found', { status: 404 }) +} +``` + +::: info +Learn more about [oRPC OpenAPI Handler](/docs/openapi/openapi-handler). +::: + +## Error Formatting + +The `toORPCRouter` does not support [tRPC Error Formatting](https://trpc.io/docs/server/error-formatting). You should catch errors and format them manually using interceptors: + +```ts +const handler = new OpenAPIHandler(orpcRouter, { + interceptors: [ + onError((error) => { + if ( + error instanceof ORPCError + && error.cause instanceof TRPCError + && error.cause.cause instanceof ZodError + ) { + throw new ORPCError('INPUT_VALIDATION_FAILED', { + status: 422, + data: error.cause.cause.flatten(), + cause: error.cause.cause, + }) + } + }) + ], +}) +``` From c681275e6ed9f99c7ed891ac9fea74931c6628ed Mon Sep 17 00:00:00 2001 From: unnoq Date: Fri, 27 Jun 2025 21:21:25 +0700 Subject: [PATCH 5/7] version --- packages/trpc/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/trpc/package.json b/packages/trpc/package.json index ebbb716ea..09aa4dd28 100644 --- a/packages/trpc/package.json +++ b/packages/trpc/package.json @@ -1,7 +1,7 @@ { "name": "@orpc/trpc", "type": "module", - "version": "1.5.2", + "version": "0.0.0", "license": "MIT", "homepage": "https://orpc.unnoq.com", "repository": { From 23532b32338285f465434b54dbfaf495dd8733ec Mon Sep 17 00:00:00 2001 From: unnoq Date: Fri, 27 Jun 2025 21:26:43 +0700 Subject: [PATCH 6/7] head links --- apps/content/docs/openapi/integrations/trpc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/content/docs/openapi/integrations/trpc.md b/apps/content/docs/openapi/integrations/trpc.md index e1c0cf999..40eb0b9ac 100644 --- a/apps/content/docs/openapi/integrations/trpc.md +++ b/apps/content/docs/openapi/integrations/trpc.md @@ -72,7 +72,7 @@ const spec = await openAPIGenerator.generate(orpcRouter, { ``` ::: info -Learn more about [oRPC OpenAPI Specification Generation](/docs/openapi/openapi-specification-generation). +Learn more about [oRPC OpenAPI Specification Generation](/docs/openapi/openapi-specification). ::: ### Request Handling From 4fa2e6008d13cda51fb39dfca571b8a3a9b79a1a Mon Sep 17 00:00:00 2001 From: unnoq Date: Fri, 27 Jun 2025 21:30:46 +0700 Subject: [PATCH 7/7] jsdocs --- packages/trpc/src/to-orpc-router.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/trpc/src/to-orpc-router.ts b/packages/trpc/src/to-orpc-router.ts index ca75c2318..f9f7b16ba 100644 --- a/packages/trpc/src/to-orpc-router.ts +++ b/packages/trpc/src/to-orpc-router.ts @@ -25,6 +25,11 @@ export type experimental_ToORPCRouterResult( router: T, ): experimental_ToORPCRouterResult< @@ -108,6 +113,10 @@ function toORPCProcedure(procedure: AnyProcedure) { }) } +/** + * Wraps a TRPC schema to disable validation in the ORPC context. + * This is necessary because tRPC procedure calling already validates the input/output, + */ function toDisabledStandardSchema(schema: undefined | Parser): undefined | ORPC.Schema { if (!isTypescriptObject(schema) || !('~standard' in schema) || !isTypescriptObject(schema['~standard'])) { return undefined