diff --git a/packages/blog/src/integrations/api/router.ts b/packages/blog/src/integrations/api/router.ts index 5c24c2f..af74435 100644 --- a/packages/blog/src/integrations/api/router.ts +++ b/packages/blog/src/integrations/api/router.ts @@ -43,3 +43,13 @@ export const blogRouter = router({ }); export type BlogRouter = typeof blogRouter; + +/** + * This feature's slice as it is mounted in the app router (the `blog` key + * in @repo/core-api). UI hooks pass it to `useTRPC()` so + * they stay fully typed without core-trpc (or this feature's client code) + * depending on the composition layer. Type-only: erased at compile time. + */ +export type BlogAppSlice = ReturnType< + typeof router<{ blog: typeof blogRouter }> +>; diff --git a/packages/blog/src/ui/hooks/use-article-by-slug.ts b/packages/blog/src/ui/hooks/use-article-by-slug.ts index 7c688d0..3e29494 100644 --- a/packages/blog/src/ui/hooks/use-article-by-slug.ts +++ b/packages/blog/src/ui/hooks/use-article-by-slug.ts @@ -2,10 +2,11 @@ import { useSuspenseQuery } from "@tanstack/react-query"; import { useTRPC } from "@repo/core-trpc"; +import type { BlogAppSlice } from "../../integrations/api/router"; import type { Article } from "../../entities/models/article"; export function useArticleBySlug(slug: string) { - const trpc = useTRPC(); + const trpc = useTRPC(); return useSuspenseQuery(trpc.blog.articleBySlug.queryOptions({ slug })) as { data: Article | null; }; diff --git a/packages/blog/src/ui/hooks/use-article-list.ts b/packages/blog/src/ui/hooks/use-article-list.ts index 2898ae9..19f0b20 100644 --- a/packages/blog/src/ui/hooks/use-article-list.ts +++ b/packages/blog/src/ui/hooks/use-article-list.ts @@ -2,13 +2,14 @@ import { useSuspenseQuery } from "@tanstack/react-query"; import { useTRPC } from "@repo/core-trpc"; +import type { BlogAppSlice } from "../../integrations/api/router"; import type { Article } from "../../entities/models/article"; export function useArticleList(options?: { status?: "draft" | "published"; limit?: number; }) { - const trpc = useTRPC(); + const trpc = useTRPC(); return useSuspenseQuery( trpc.blog.listArticles.queryOptions({ status: options?.status ?? "published", diff --git a/packages/core-trpc/package.json b/packages/core-trpc/package.json index a41588c..60f9ce2 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.17.0", "@trpc/react-query": "^11.17.0", 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/packages/marketing-pages/src/integrations/api/router.ts b/packages/marketing-pages/src/integrations/api/router.ts index 024ec54..d7e1f64 100644 --- a/packages/marketing-pages/src/integrations/api/router.ts +++ b/packages/marketing-pages/src/integrations/api/router.ts @@ -32,3 +32,13 @@ export const marketingPagesRouter = router({ }); export type MarketingPagesRouter = typeof marketingPagesRouter; + +/** + * This feature's slice as it is mounted in the app router (the `marketingPages` key + * in @repo/core-api). UI hooks pass it to `useTRPC()` so + * they stay fully typed without core-trpc (or this feature's client code) + * depending on the composition layer. Type-only: erased at compile time. + */ +export type MarketingPagesAppSlice = ReturnType< + typeof router<{ marketingPages: typeof marketingPagesRouter }> +>; diff --git a/packages/marketing-pages/src/ui/hooks/use-page-by-slug.ts b/packages/marketing-pages/src/ui/hooks/use-page-by-slug.ts index 284dbc0..bc94ee4 100644 --- a/packages/marketing-pages/src/ui/hooks/use-page-by-slug.ts +++ b/packages/marketing-pages/src/ui/hooks/use-page-by-slug.ts @@ -2,10 +2,11 @@ import { useSuspenseQuery } from "@tanstack/react-query"; import { useTRPC } from "@repo/core-trpc"; +import type { MarketingPagesAppSlice } from "../../integrations/api/router"; import type { Page } from "../../entities/models/page"; export function usePageBySlug(slug: string) { - const trpc = useTRPC(); + const trpc = useTRPC(); // A missing slug now rejects with NOT_FOUND (PageNotFoundError), so a // resolved query always carries a Page. return useSuspenseQuery( diff --git a/packages/marketing-pages/src/ui/hooks/use-site-settings.ts b/packages/marketing-pages/src/ui/hooks/use-site-settings.ts index c0ed45e..db4ddf7 100644 --- a/packages/marketing-pages/src/ui/hooks/use-site-settings.ts +++ b/packages/marketing-pages/src/ui/hooks/use-site-settings.ts @@ -2,10 +2,11 @@ import { useSuspenseQuery } from "@tanstack/react-query"; import { useTRPC } from "@repo/core-trpc"; +import type { MarketingPagesAppSlice } from "../../integrations/api/router"; import type { SiteSettings } from "../../entities/models/site-settings"; export function useSiteSettings() { - const trpc = useTRPC(); + const trpc = useTRPC(); return useSuspenseQuery( trpc.marketingPages.siteSettings.queryOptions({}), ) as { data: SiteSettings }; diff --git a/packages/navigation/src/integrations/api/router.ts b/packages/navigation/src/integrations/api/router.ts index 16ccb76..dc2c467 100644 --- a/packages/navigation/src/integrations/api/router.ts +++ b/packages/navigation/src/integrations/api/router.ts @@ -9,14 +9,22 @@ import type { IGetHeaderController } from "../../interface-adapters/controllers/ import { navigationProcedure } from "./procedures"; export const navigationRouter = router({ - header: navigationProcedure - .input(getHeaderInputSchema) - .query(({ input }) => { - const ctrl = navigationContainer.get( - NAVIGATION_SYMBOLS.IGetHeaderController, - ); - return ctrl(input); - }), + header: navigationProcedure.input(getHeaderInputSchema).query(({ input }) => { + const ctrl = navigationContainer.get( + NAVIGATION_SYMBOLS.IGetHeaderController, + ); + return ctrl(input); + }), }); export type NavigationRouter = typeof navigationRouter; + +/** + * This feature's slice as it is mounted in the app router (the `navigation` key + * in @repo/core-api). UI hooks pass it to `useTRPC()` so + * they stay fully typed without core-trpc (or this feature's client code) + * depending on the composition layer. Type-only: erased at compile time. + */ +export type NavigationAppSlice = ReturnType< + typeof router<{ navigation: typeof navigationRouter }> +>; diff --git a/packages/navigation/src/ui/hooks/use-header.ts b/packages/navigation/src/ui/hooks/use-header.ts index a9c618c..3926557 100644 --- a/packages/navigation/src/ui/hooks/use-header.ts +++ b/packages/navigation/src/ui/hooks/use-header.ts @@ -2,10 +2,11 @@ import { useSuspenseQuery } from "@tanstack/react-query"; import { useTRPC } from "@repo/core-trpc"; +import type { NavigationAppSlice } from "../../integrations/api/router"; // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export function useHeader() { - const trpc = useTRPC(); + const trpc = useTRPC(); return useSuspenseQuery(trpc.navigation.header.queryOptions({})) as { data: { items: { label: string; href: string; external: boolean }[]; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2b4dfd4..c13b64a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -930,9 +930,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)