From c2841cf35277a79ac9d6b4e136991691a99f3202 Mon Sep 17 00:00:00 2001 From: Danijel Martinek Date: Mon, 13 Jul 2026 06:09:10 +0200 Subject: [PATCH] refactor(core-trpc): make the tRPC context router-agnostic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ports the type-safety/ergonomics half of the upstream cycle-break: core-trpc no longer imports AppRouter from @repo/core-api (an illegal core -> core-composition boundary edge). The runtime context and both providers are now AnyTRPCRouter-generic; consumers inject their router type via useTRPC(). The clean-slate tree is already cycle-free (no kept feature consumes core-trpc), so this is purely the architecture/boundary improvement. The per-feature app-slice types the upstream commit added for the demo features are intentionally skipped: no kept feature consumes useTRPC yet. Also covers the router-agnostic useTRPC delegation: the refactor turned useTRPC into a wrapper function whose body was unexercised, dropping client.ts below the coverage floor — client.test.ts stubs the context factory and asserts the delegation, restoring client.ts to 100%. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_016j8z4VHjedXDTjEDNg7qHK --- packages/core-trpc/package.json | 1 - packages/core-trpc/src/client.test.ts | 19 ++++++++++++++++++- packages/core-trpc/src/client.ts | 18 ++++++++++++++++-- packages/core-trpc/src/index.ts | 1 - .../core-trpc/src/providers/next-provider.tsx | 6 ++++-- .../src/providers/tanstack-provider.tsx | 6 ++++-- pnpm-lock.yaml | 3 --- 7 files changed, 42 insertions(+), 12 deletions(-) diff --git a/packages/core-trpc/package.json b/packages/core-trpc/package.json index 518bb50..d9b9db4 100644 --- a/packages/core-trpc/package.json +++ b/packages/core-trpc/package.json @@ -15,7 +15,6 @@ "test": "vitest run --passWithNoTests" }, "dependencies": { - "@repo/core-api": "workspace:*", "@tanstack/react-query": "^5.66.0", "@trpc/client": "^11.18.0", "@trpc/server": "^11.18.0", diff --git a/packages/core-trpc/src/client.test.ts b/packages/core-trpc/src/client.test.ts index e998a06..9d879d2 100644 --- a/packages/core-trpc/src/client.test.ts +++ b/packages/core-trpc/src/client.test.ts @@ -1,4 +1,15 @@ -import { describe, it, expect } from "vitest"; +import { describe, it, expect, vi } from "vitest"; + +// Stub the tRPC context factory so useTRPC() can run outside a React render: +// the real context.useTRPC() requires a mounted TRPCProvider. The stubbed +// proxy lets us exercise the router-agnostic delegation (client.ts:useTRPC). +vi.mock("@trpc/tanstack-react-query", () => ({ + createTRPCContext: () => ({ + TRPCProvider: () => null, + useTRPC: () => ({ __proxy: "trpc-options-proxy" }), + }), +})); + import { useTRPC, TRPCProvider } from "./client"; describe("core-trpc client exports", () => { @@ -9,4 +20,10 @@ describe("core-trpc client exports", () => { it("exports TRPCProvider component", () => { expect(TRPCProvider).toBeTypeOf("function"); }); + + it("useTRPC delegates to the underlying router-agnostic tRPC proxy", () => { + // The generic is a compile-time-only cast; at runtime it returns whatever + // the shared context proxy yields. + expect(useTRPC()).toEqual({ __proxy: "trpc-options-proxy" }); + }); }); diff --git a/packages/core-trpc/src/client.ts b/packages/core-trpc/src/client.ts index 722b8c1..b0c68bf 100644 --- a/packages/core-trpc/src/client.ts +++ b/packages/core-trpc/src/client.ts @@ -1,6 +1,20 @@ "use client"; import { createTRPCContext } from "@trpc/tanstack-react-query"; -import type { AppRouter } from "@repo/core-api"; +import type { TRPCOptionsProxy } from "@trpc/tanstack-react-query"; +import type { AnyTRPCRouter } from "@trpc/server"; -export const { TRPCProvider, useTRPC } = createTRPCContext(); +// core-trpc must not depend on @repo/core-api (core → core-composition is an +// illegal boundary edge and creates a package cycle through the features). +// The runtime context is router-agnostic; consumers inject their router type +// as a generic: features pass their app-slice type (e.g. `BlogAppSlice`), +// apps may pass the full AppRouter. +const context = createTRPCContext(); + +export const TRPCProvider = context.TRPCProvider; + +export function useTRPC< + TRouter extends AnyTRPCRouter = AnyTRPCRouter, +>(): TRPCOptionsProxy { + return context.useTRPC() as unknown as TRPCOptionsProxy; +} diff --git a/packages/core-trpc/src/index.ts b/packages/core-trpc/src/index.ts index 7b27b5d..94a24e8 100644 --- a/packages/core-trpc/src/index.ts +++ b/packages/core-trpc/src/index.ts @@ -1,3 +1,2 @@ export { useTRPC, TRPCProvider } from "./client"; export { getQueryClient } from "./query-client"; -export type { AppRouter } from "@repo/core-api"; diff --git a/packages/core-trpc/src/providers/next-provider.tsx b/packages/core-trpc/src/providers/next-provider.tsx index dc4b9d8..0b24838 100644 --- a/packages/core-trpc/src/providers/next-provider.tsx +++ b/packages/core-trpc/src/providers/next-provider.tsx @@ -4,7 +4,7 @@ import { useState } from "react"; import { QueryClientProvider } from "@tanstack/react-query"; import { createTRPCClient, httpBatchLink } from "@trpc/client"; import superjson from "superjson"; -import type { AppRouter } from "@repo/core-api"; +import type { AnyTRPCRouter } from "@trpc/server"; import { TRPCProvider } from "../client"; import { getQueryClient } from "../query-client"; @@ -22,7 +22,9 @@ export function NextTrpcProvider({ }) { const [queryClient] = useState(() => getQueryClient()); const [trpcClient] = useState(() => - createTRPCClient({ + // Router-agnostic on purpose: procedure typing comes from useTRPC + // at the call sites, not from the wire-level client. + createTRPCClient({ links: [ httpBatchLink({ url: `${getBaseUrl()}${trpcUrl}`, diff --git a/packages/core-trpc/src/providers/tanstack-provider.tsx b/packages/core-trpc/src/providers/tanstack-provider.tsx index 0f48647..c5e46dc 100644 --- a/packages/core-trpc/src/providers/tanstack-provider.tsx +++ b/packages/core-trpc/src/providers/tanstack-provider.tsx @@ -4,7 +4,7 @@ import { useState } from "react"; import { QueryClientProvider } from "@tanstack/react-query"; import { createTRPCClient, httpBatchLink } from "@trpc/client"; import superjson from "superjson"; -import type { AppRouter } from "@repo/core-api"; +import type { AnyTRPCRouter } from "@trpc/server"; import { TRPCProvider } from "../client"; import { getQueryClient } from "../query-client"; @@ -17,7 +17,9 @@ export function TanstackTrpcProvider({ }) { const [queryClient] = useState(() => getQueryClient()); const [trpcClient] = useState(() => - createTRPCClient({ + // Router-agnostic on purpose: procedure typing comes from useTRPC + // at the call sites, not from the wire-level client. + createTRPCClient({ links: [httpBatchLink({ url: trpcUrl, transformer: superjson })], }), ); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 24affeb..77bca3c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -760,9 +760,6 @@ importers: packages/core-trpc: dependencies: - "@repo/core-api": - specifier: workspace:* - version: link:../core-api "@tanstack/react-query": specifier: ^5.66.0 version: 5.96.2(react@19.2.4)