refactor(core-trpc): break the core-api package cycle
core-trpc imported AppRouter (type-only) from core-api, which depends on the features, which depend on core-trpc — a cycle that killed every turbo graph walk (lint/build/CI) and an illegal core -> core-composition boundary edge. The tRPC context is now router-agnostic (AnyTRPCRouter); useTRPC takes the router type as a generic and each feature exports a type-only app-slice (e.g. BlogAppSlice) mirroring its mount key, so UI hooks keep full procedure typing without touching the composition layer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -43,3 +43,13 @@ export const blogRouter = router({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export type BlogRouter = typeof blogRouter;
|
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<BlogAppSlice>()` 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 }>
|
||||||
|
>;
|
||||||
|
|||||||
@@ -2,10 +2,11 @@
|
|||||||
|
|
||||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||||
import { useTRPC } from "@repo/core-trpc";
|
import { useTRPC } from "@repo/core-trpc";
|
||||||
|
import type { BlogAppSlice } from "../../integrations/api/router";
|
||||||
import type { Article } from "../../entities/models/article";
|
import type { Article } from "../../entities/models/article";
|
||||||
|
|
||||||
export function useArticleBySlug(slug: string) {
|
export function useArticleBySlug(slug: string) {
|
||||||
const trpc = useTRPC();
|
const trpc = useTRPC<BlogAppSlice>();
|
||||||
return useSuspenseQuery(trpc.blog.articleBySlug.queryOptions({ slug })) as {
|
return useSuspenseQuery(trpc.blog.articleBySlug.queryOptions({ slug })) as {
|
||||||
data: Article | null;
|
data: Article | null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,13 +2,14 @@
|
|||||||
|
|
||||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||||
import { useTRPC } from "@repo/core-trpc";
|
import { useTRPC } from "@repo/core-trpc";
|
||||||
|
import type { BlogAppSlice } from "../../integrations/api/router";
|
||||||
import type { Article } from "../../entities/models/article";
|
import type { Article } from "../../entities/models/article";
|
||||||
|
|
||||||
export function useArticleList(options?: {
|
export function useArticleList(options?: {
|
||||||
status?: "draft" | "published";
|
status?: "draft" | "published";
|
||||||
limit?: number;
|
limit?: number;
|
||||||
}) {
|
}) {
|
||||||
const trpc = useTRPC();
|
const trpc = useTRPC<BlogAppSlice>();
|
||||||
return useSuspenseQuery(
|
return useSuspenseQuery(
|
||||||
trpc.blog.listArticles.queryOptions({
|
trpc.blog.listArticles.queryOptions({
|
||||||
status: options?.status ?? "published",
|
status: options?.status ?? "published",
|
||||||
|
|||||||
@@ -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.17.0",
|
"@trpc/client": "^11.17.0",
|
||||||
"@trpc/react-query": "^11.17.0",
|
"@trpc/react-query": "^11.17.0",
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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";
|
|
||||||
|
|||||||
@@ -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}`,
|
||||||
|
|||||||
@@ -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 })],
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -32,3 +32,13 @@ export const marketingPagesRouter = router({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export type MarketingPagesRouter = typeof marketingPagesRouter;
|
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<MarketingPagesAppSlice>()` 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 }>
|
||||||
|
>;
|
||||||
|
|||||||
@@ -2,10 +2,11 @@
|
|||||||
|
|
||||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||||
import { useTRPC } from "@repo/core-trpc";
|
import { useTRPC } from "@repo/core-trpc";
|
||||||
|
import type { MarketingPagesAppSlice } from "../../integrations/api/router";
|
||||||
import type { Page } from "../../entities/models/page";
|
import type { Page } from "../../entities/models/page";
|
||||||
|
|
||||||
export function usePageBySlug(slug: string) {
|
export function usePageBySlug(slug: string) {
|
||||||
const trpc = useTRPC();
|
const trpc = useTRPC<MarketingPagesAppSlice>();
|
||||||
// A missing slug now rejects with NOT_FOUND (PageNotFoundError), so a
|
// A missing slug now rejects with NOT_FOUND (PageNotFoundError), so a
|
||||||
// resolved query always carries a Page.
|
// resolved query always carries a Page.
|
||||||
return useSuspenseQuery(
|
return useSuspenseQuery(
|
||||||
|
|||||||
@@ -2,10 +2,11 @@
|
|||||||
|
|
||||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||||
import { useTRPC } from "@repo/core-trpc";
|
import { useTRPC } from "@repo/core-trpc";
|
||||||
|
import type { MarketingPagesAppSlice } from "../../integrations/api/router";
|
||||||
import type { SiteSettings } from "../../entities/models/site-settings";
|
import type { SiteSettings } from "../../entities/models/site-settings";
|
||||||
|
|
||||||
export function useSiteSettings() {
|
export function useSiteSettings() {
|
||||||
const trpc = useTRPC();
|
const trpc = useTRPC<MarketingPagesAppSlice>();
|
||||||
return useSuspenseQuery(
|
return useSuspenseQuery(
|
||||||
trpc.marketingPages.siteSettings.queryOptions({}),
|
trpc.marketingPages.siteSettings.queryOptions({}),
|
||||||
) as { data: SiteSettings };
|
) as { data: SiteSettings };
|
||||||
|
|||||||
@@ -9,9 +9,7 @@ import type { IGetHeaderController } from "../../interface-adapters/controllers/
|
|||||||
import { navigationProcedure } from "./procedures";
|
import { navigationProcedure } from "./procedures";
|
||||||
|
|
||||||
export const navigationRouter = router({
|
export const navigationRouter = router({
|
||||||
header: navigationProcedure
|
header: navigationProcedure.input(getHeaderInputSchema).query(({ input }) => {
|
||||||
.input(getHeaderInputSchema)
|
|
||||||
.query(({ input }) => {
|
|
||||||
const ctrl = navigationContainer.get<IGetHeaderController>(
|
const ctrl = navigationContainer.get<IGetHeaderController>(
|
||||||
NAVIGATION_SYMBOLS.IGetHeaderController,
|
NAVIGATION_SYMBOLS.IGetHeaderController,
|
||||||
);
|
);
|
||||||
@@ -20,3 +18,13 @@ export const navigationRouter = router({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export type NavigationRouter = typeof navigationRouter;
|
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<NavigationAppSlice>()` 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 }>
|
||||||
|
>;
|
||||||
|
|||||||
@@ -2,10 +2,11 @@
|
|||||||
|
|
||||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||||
import { useTRPC } from "@repo/core-trpc";
|
import { useTRPC } from "@repo/core-trpc";
|
||||||
|
import type { NavigationAppSlice } from "../../integrations/api/router";
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||||
export function useHeader() {
|
export function useHeader() {
|
||||||
const trpc = useTRPC();
|
const trpc = useTRPC<NavigationAppSlice>();
|
||||||
return useSuspenseQuery(trpc.navigation.header.queryOptions({})) as {
|
return useSuspenseQuery(trpc.navigation.header.queryOptions({})) as {
|
||||||
data: {
|
data: {
|
||||||
items: { label: string; href: string; external: boolean }[];
|
items: { label: string; href: string; external: boolean }[];
|
||||||
|
|||||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -930,9 +930,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)
|
||||||
|
|||||||
Reference in New Issue
Block a user