refactor(core-trpc): make the tRPC context router-agnostic

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<TRouter>().

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016j8z4VHjedXDTjEDNg7qHK
This commit is contained in:
2026-07-13 06:09:10 +02:00
parent 361b28da99
commit c2841cf352
7 changed files with 42 additions and 12 deletions

View File

@@ -15,7 +15,6 @@
"test": "vitest run --passWithNoTests" "test": "vitest run --passWithNoTests"
}, },
"dependencies": { "dependencies": {
"@repo/core-api": "workspace:*",
"@tanstack/react-query": "^5.66.0", "@tanstack/react-query": "^5.66.0",
"@trpc/client": "^11.18.0", "@trpc/client": "^11.18.0",
"@trpc/server": "^11.18.0", "@trpc/server": "^11.18.0",

View File

@@ -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"; import { useTRPC, TRPCProvider } from "./client";
describe("core-trpc client exports", () => { describe("core-trpc client exports", () => {
@@ -9,4 +20,10 @@ describe("core-trpc client exports", () => {
it("exports TRPCProvider component", () => { it("exports TRPCProvider component", () => {
expect(TRPCProvider).toBeTypeOf("function"); 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" });
});
}); });

View File

@@ -1,6 +1,20 @@
"use client"; "use client";
import { createTRPCContext } from "@trpc/tanstack-react-query"; 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<AppRouter>(); // 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<AnyTRPCRouter>();
export const TRPCProvider = context.TRPCProvider;
export function useTRPC<
TRouter extends AnyTRPCRouter = AnyTRPCRouter,
>(): TRPCOptionsProxy<TRouter> {
return context.useTRPC() as unknown as TRPCOptionsProxy<TRouter>;
}

View File

@@ -1,3 +1,2 @@
export { useTRPC, TRPCProvider } from "./client"; export { useTRPC, TRPCProvider } from "./client";
export { getQueryClient } from "./query-client"; export { getQueryClient } from "./query-client";
export type { AppRouter } from "@repo/core-api";

View File

@@ -4,7 +4,7 @@ import { useState } from "react";
import { QueryClientProvider } from "@tanstack/react-query"; import { QueryClientProvider } from "@tanstack/react-query";
import { createTRPCClient, httpBatchLink } from "@trpc/client"; import { createTRPCClient, httpBatchLink } from "@trpc/client";
import superjson from "superjson"; import superjson from "superjson";
import type { AppRouter } from "@repo/core-api"; import type { AnyTRPCRouter } from "@trpc/server";
import { TRPCProvider } from "../client"; import { TRPCProvider } from "../client";
import { getQueryClient } from "../query-client"; import { getQueryClient } from "../query-client";
@@ -22,7 +22,9 @@ export function NextTrpcProvider({
}) { }) {
const [queryClient] = useState(() => getQueryClient()); const [queryClient] = useState(() => getQueryClient());
const [trpcClient] = useState(() => const [trpcClient] = useState(() =>
createTRPCClient<AppRouter>({ // Router-agnostic on purpose: procedure typing comes from useTRPC<TRouter>
// at the call sites, not from the wire-level client.
createTRPCClient<AnyTRPCRouter>({
links: [ links: [
httpBatchLink({ httpBatchLink({
url: `${getBaseUrl()}${trpcUrl}`, url: `${getBaseUrl()}${trpcUrl}`,

View File

@@ -4,7 +4,7 @@ import { useState } from "react";
import { QueryClientProvider } from "@tanstack/react-query"; import { QueryClientProvider } from "@tanstack/react-query";
import { createTRPCClient, httpBatchLink } from "@trpc/client"; import { createTRPCClient, httpBatchLink } from "@trpc/client";
import superjson from "superjson"; import superjson from "superjson";
import type { AppRouter } from "@repo/core-api"; import type { AnyTRPCRouter } from "@trpc/server";
import { TRPCProvider } from "../client"; import { TRPCProvider } from "../client";
import { getQueryClient } from "../query-client"; import { getQueryClient } from "../query-client";
@@ -17,7 +17,9 @@ export function TanstackTrpcProvider({
}) { }) {
const [queryClient] = useState(() => getQueryClient()); const [queryClient] = useState(() => getQueryClient());
const [trpcClient] = useState(() => const [trpcClient] = useState(() =>
createTRPCClient<AppRouter>({ // Router-agnostic on purpose: procedure typing comes from useTRPC<TRouter>
// at the call sites, not from the wire-level client.
createTRPCClient<AnyTRPCRouter>({
links: [httpBatchLink({ url: trpcUrl, transformer: superjson })], links: [httpBatchLink({ url: trpcUrl, transformer: superjson })],
}), }),
); );

3
pnpm-lock.yaml generated
View File

@@ -760,9 +760,6 @@ importers:
packages/core-trpc: packages/core-trpc:
dependencies: dependencies:
"@repo/core-api":
specifier: workspace:*
version: link:../core-api
"@tanstack/react-query": "@tanstack/react-query":
specifier: ^5.66.0 specifier: ^5.66.0
version: 5.96.2(react@19.2.4) version: 5.96.2(react@19.2.4)