diff --git a/package.json b/package.json index 0451699a..01186555 100755 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "migrate": "prisma migrate dev --schema ./packages/canyon-backend/prisma/schema.prisma", "preinstall": "node ./scripts/check.js", "prepare": "husky", - "test": "pnpm -r do-test" + "test": "pnpm -r do-test", + "start-canyon-ut": "pnpm -r do-start-canyon-ut" }, "devDependencies": { "@commitlint/cli": "^19.5.0", diff --git a/packages/canyon-ut/.eslintrc.cjs b/packages/canyon-ut/.eslintrc.cjs new file mode 100644 index 00000000..a92fb0bc --- /dev/null +++ b/packages/canyon-ut/.eslintrc.cjs @@ -0,0 +1,42 @@ +/** @type {import("eslint").Linter.Config} */ +const config = { + "parser": "@typescript-eslint/parser", + "parserOptions": { + "project": true + }, + "plugins": [ + "@typescript-eslint" + ], + "extends": [ + "next/core-web-vitals", + "plugin:@typescript-eslint/recommended-type-checked", + "plugin:@typescript-eslint/stylistic-type-checked" + ], + "rules": { + "@typescript-eslint/array-type": "off", + "@typescript-eslint/consistent-type-definitions": "off", + "@typescript-eslint/consistent-type-imports": [ + "warn", + { + "prefer": "type-imports", + "fixStyle": "inline-type-imports" + } + ], + "@typescript-eslint/no-unused-vars": [ + "warn", + { + "argsIgnorePattern": "^_" + } + ], + "@typescript-eslint/require-await": "off", + "@typescript-eslint/no-misused-promises": [ + "error", + { + "checksVoidReturn": { + "attributes": false + } + } + ] + } +} +module.exports = config; \ No newline at end of file diff --git a/packages/canyon-ut/.gitignore b/packages/canyon-ut/.gitignore new file mode 100644 index 00000000..c24a8359 --- /dev/null +++ b/packages/canyon-ut/.gitignore @@ -0,0 +1,46 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# database +/prisma/db.sqlite +/prisma/db.sqlite-journal +db.sqlite + +# next.js +/.next/ +/out/ +next-env.d.ts + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# local env files +# do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables +.env +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo + +# idea files +.idea \ No newline at end of file diff --git a/packages/canyon-ut/README.md b/packages/canyon-ut/README.md new file mode 100644 index 00000000..e69de29b diff --git a/packages/canyon-ut/next.config.js b/packages/canyon-ut/next.config.js new file mode 100644 index 00000000..853e4a06 --- /dev/null +++ b/packages/canyon-ut/next.config.js @@ -0,0 +1,21 @@ +/** + * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful + * for Docker builds. + */ + +const dotenv = await import('dotenv'); +dotenv.config({ path: '../../.env' }); + +await import("./src/env.js"); + +/** @type {import("next").NextConfig} */ +const config = { + typescript: { + ignoreBuildErrors: true, + }, + eslint: { + ignoreDuringBuilds: true, + }, +}; + +export default config; diff --git a/packages/canyon-ut/package.json b/packages/canyon-ut/package.json new file mode 100644 index 00000000..6f24d8cc --- /dev/null +++ b/packages/canyon-ut/package.json @@ -0,0 +1,49 @@ +{ + "name": "canyon-ut", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "do-build": "next build", + "dev": "next dev", + "postinstall": "prisma generate", + "lint": "next lint", + "do-start-canyon-ut": "next start --port 8080" + }, + "dependencies": { + "@prisma/client": "^5.14.0", + "@t3-oss/env-nextjs": "^0.10.1", + "@tanstack/react-query": "^5.50.0", + "@trpc/client": "^11.0.0-rc.446", + "@trpc/react-query": "^11.0.0-rc.446", + "@trpc/server": "^11.0.0-rc.446", + "dotenv": "^16.4.5", + "geist": "^1.3.0", + "next": "^14.2.4", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "server-only": "^0.0.1", + "superjson": "^2.2.1", + "zod": "^3.23.3" + }, + "devDependencies": { + "@types/eslint": "^8.56.10", + "@types/node": "^20.14.10", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@typescript-eslint/eslint-plugin": "^8.1.0", + "@typescript-eslint/parser": "^8.1.0", + "eslint": "^8.57.0", + "eslint-config-next": "^14.2.4", + "postcss": "^8.4.39", + "prettier": "^3.3.2", + "prettier-plugin-tailwindcss": "^0.6.5", + "prisma": "^5.14.0", + "tailwindcss": "^3.4.3", + "typescript": "^5.5.3" + }, + "ct3aMetadata": { + "initVersion": "7.37.0" + }, + "packageManager": "pnpm@9.9.0" +} diff --git a/packages/canyon-ut/postcss.config.cjs b/packages/canyon-ut/postcss.config.cjs new file mode 100644 index 00000000..4cdb2f43 --- /dev/null +++ b/packages/canyon-ut/postcss.config.cjs @@ -0,0 +1,7 @@ +const config = { + plugins: { + tailwindcss: {}, + }, +}; + +module.exports = config; diff --git a/packages/canyon-ut/prettier.config.js b/packages/canyon-ut/prettier.config.js new file mode 100644 index 00000000..b2d59b46 --- /dev/null +++ b/packages/canyon-ut/prettier.config.js @@ -0,0 +1,6 @@ +/** @type {import('prettier').Config & import('prettier-plugin-tailwindcss').PluginOptions} */ +const config = { + plugins: ["prettier-plugin-tailwindcss"], +}; + +export default config; diff --git a/packages/canyon-ut/prisma/schema.prisma b/packages/canyon-ut/prisma/schema.prisma new file mode 100644 index 00000000..448c81ab --- /dev/null +++ b/packages/canyon-ut/prisma/schema.prisma @@ -0,0 +1,37 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model Coverage { + id String @id @default(cuid()) + sha String @map("sha") + branch String + projectID String @map("project_id") + // 代码覆盖率 + branchesTotal Int @map("branches_total") + branchesCovered Int @map("branches_covered") + functionsTotal Int @map("functions_total") + functionsCovered Int @map("functions_covered") + linesTotal Int @map("lines_total") + linesCovered Int @map("lines_covered") + statementsTotal Int @map("statements_total") + statementsCovered Int @map("statements_covered") + newlinesTotal Int @map("newlines_total") + newlinesCovered Int @map("newlines_covered") + summary String //zstd+pb 必要性,提高概览页面查询速度 + // 代码覆盖率详情 + // 覆盖率实体 + hit String //zstd+pb + // 通用 + createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(3) + + @@map("ut_coverage") +} diff --git a/packages/canyon-ut/public/favicon.ico b/packages/canyon-ut/public/favicon.ico new file mode 100644 index 00000000..60c702aa Binary files /dev/null and b/packages/canyon-ut/public/favicon.ico differ diff --git a/packages/canyon-ut/src/app/_components/post.tsx b/packages/canyon-ut/src/app/_components/post.tsx new file mode 100644 index 00000000..e33fac30 --- /dev/null +++ b/packages/canyon-ut/src/app/_components/post.tsx @@ -0,0 +1,50 @@ +"use client"; + +import { useState } from "react"; + +import { api } from "~/trpc/react"; + +export function LatestCoverage() { + const [latestCoverage] = api.coverage.getLatest.useSuspenseQuery(); + + const utils = api.useUtils(); + const [name, setName] = useState(""); + const createCoverage = api.coverage.create.useMutation({ + onSuccess: async () => { + await utils.coverage.invalidate(); + setName(""); + }, + }); + + return ( +
+ {latestCoverage ? ( +

Your most recent coverage: {latestCoverage.name}

+ ) : ( +

You have no coverages yet.

+ )} +
{ + e.preventDefault(); + createCoverage.mutate({ name }); + }} + className="flex flex-col gap-2" + > + setName(e.target.value)} + className="w-full rounded-full px-4 py-2 text-black" + /> + +
+
+ ); +} diff --git a/packages/canyon-ut/src/app/api/health/route.ts b/packages/canyon-ut/src/app/api/health/route.ts new file mode 100644 index 00000000..b59e1ea9 --- /dev/null +++ b/packages/canyon-ut/src/app/api/health/route.ts @@ -0,0 +1,8 @@ +import {NextRequest, NextResponse} from "next/server"; + +const healthCheckHandler = (_req: NextRequest) => { + + return NextResponse.json('success'); +} + +export { healthCheckHandler as GET }; diff --git a/packages/canyon-ut/src/app/api/trpc/[trpc]/route.ts b/packages/canyon-ut/src/app/api/trpc/[trpc]/route.ts new file mode 100644 index 00000000..f194ae57 --- /dev/null +++ b/packages/canyon-ut/src/app/api/trpc/[trpc]/route.ts @@ -0,0 +1,41 @@ +import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; +import {type NextRequest, NextResponse} from "next/server"; + +import { env } from "~/env"; +import { appRouter } from "~/server/api/root"; +import { createTRPCContext } from "~/server/api/trpc"; + +/** + * This wraps the `createTRPCContext` helper and provides the required context for the tRPC API when + * handling a HTTP request (e.g. when you make requests from Client Components). + */ +const createContext = async (req: NextRequest) => { + return createTRPCContext({ + headers: req.headers, + }); +}; + +const handler = (req: NextRequest) => + fetchRequestHandler({ + endpoint: "/api/trpc", + req, + router: appRouter, + createContext: () => createContext(req), + onError: + env.NODE_ENV === "development" + ? ({ path, error }) => { + console.error( + `❌ tRPC failed on ${path ?? ""}: ${error.message}` + ); + } + : undefined, + }); + +const healthCheckHandler = (_req: NextRequest) => { + + return NextResponse.json({ + ok: true, + }) +} + +export { healthCheckHandler as GET, handler as POST }; diff --git a/packages/canyon-ut/src/app/layout.tsx b/packages/canyon-ut/src/app/layout.tsx new file mode 100644 index 00000000..c2215fff --- /dev/null +++ b/packages/canyon-ut/src/app/layout.tsx @@ -0,0 +1,24 @@ +import "~/styles/globals.css"; + +import { GeistSans } from "geist/font/sans"; +import { type Metadata } from "next"; + +import { TRPCReactProvider } from "~/trpc/react"; + +export const metadata: Metadata = { + title: "Create T3 App", + description: "Generated by create-t3-app", + icons: [{ rel: "icon", url: "/favicon.ico" }], +}; + +export default function RootLayout({ + children, +}: Readonly<{ children: React.ReactNode }>) { + return ( + + + {children} + + + ); +} diff --git a/packages/canyon-ut/src/app/page.tsx b/packages/canyon-ut/src/app/page.tsx new file mode 100644 index 00000000..978aca10 --- /dev/null +++ b/packages/canyon-ut/src/app/page.tsx @@ -0,0 +1,53 @@ +import Link from "next/link"; + +import { LatestCoverage } from "~/app/_components/post"; +import { api, HydrateClient } from "~/trpc/server"; + +export default async function Home() { + const hello = await api.coverage.hello({ text: "from tRPC" }); + + void api.coverage.getLatest.prefetch(); + + return ( + +
+
+

+ Create T3 App +

+
+ +

First Steps →

+
+ Just the basics - Everything you need to know to set up your + database and authentication. +
+ + +

Documentation →

+
+ Learn more about Create T3 App, the libraries it uses, and how + to deploy it. +
+ +
+
+

+ {hello ? hello.greeting : "Loading tRPC query..."} +

+
+ + +
+
+
+ ); +} diff --git a/packages/canyon-ut/src/env.js b/packages/canyon-ut/src/env.js new file mode 100644 index 00000000..6ca7f3e9 --- /dev/null +++ b/packages/canyon-ut/src/env.js @@ -0,0 +1,44 @@ +import { createEnv } from "@t3-oss/env-nextjs"; +import { z } from "zod"; + +export const env = createEnv({ + /** + * Specify your server-side environment variables schema here. This way you can ensure the app + * isn't built with invalid env vars. + */ + server: { + DATABASE_URL: z.string().url(), + NODE_ENV: z + .enum(["development", "test", "production"]) + .default("development"), + }, + + /** + * Specify your client-side environment variables schema here. This way you can ensure the app + * isn't built with invalid env vars. To expose them to the client, prefix them with + * `NEXT_PUBLIC_`. + */ + client: { + // NEXT_PUBLIC_CLIENTVAR: z.string(), + }, + + /** + * You can't destruct `process.env` as a regular object in the Next.js edge runtimes (e.g. + * middlewares) or client-side so we need to destruct manually. + */ + runtimeEnv: { + DATABASE_URL: process.env.DATABASE_URL, + NODE_ENV: process.env.NODE_ENV, + // NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR, + }, + /** + * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially + * useful for Docker builds. + */ + skipValidation: !!process.env.SKIP_ENV_VALIDATION, + /** + * Makes it so that empty strings are treated as undefined. `SOME_VAR: z.string()` and + * `SOME_VAR=''` will throw an error. + */ + emptyStringAsUndefined: true, +}); diff --git a/packages/canyon-ut/src/server/api/root.ts b/packages/canyon-ut/src/server/api/root.ts new file mode 100644 index 00000000..41b9dc67 --- /dev/null +++ b/packages/canyon-ut/src/server/api/root.ts @@ -0,0 +1,23 @@ +import { coverageRouter } from "~/server/api/routers/coverage"; +import { createCallerFactory, createTRPCRouter } from "~/server/api/trpc"; + +/** + * This is the primary router for your server. + * + * All routers added in /api/routers should be manually added here. + */ +export const appRouter = createTRPCRouter({ + coverage: coverageRouter, +}); + +// export type definition of API +export type AppRouter = typeof appRouter; + +/** + * Create a server-side caller for the tRPC API. + * @example + * const trpc = createCaller(createContext); + * const res = await trpc.coverage.all(); + * ^? coverage[] + */ +export const createCaller = createCallerFactory(appRouter); diff --git a/packages/canyon-ut/src/server/api/routers/coverage.ts b/packages/canyon-ut/src/server/api/routers/coverage.ts new file mode 100644 index 00000000..0293d7fb --- /dev/null +++ b/packages/canyon-ut/src/server/api/routers/coverage.ts @@ -0,0 +1,71 @@ +import { z } from "zod"; + +import { createTRPCRouter, publicProcedure } from "~/server/api/trpc"; + +export const coverageRouter = createTRPCRouter({ + hello: publicProcedure + .input(z.object({ text: z.string() })) + .query(({ input }) => { + return { + greeting: `Hello ${input.text}`, + }; + }), + + create: publicProcedure + .input(z.object({ name: z.string().min(1) })) + .mutation(async ({ ctx, input }) => { + return ctx.db.coverage.create({ + data: { + // model Coverage { + // id String @id @default(cuid()) + // sha String @map("sha") + // branch String + // projectID String @map("project_id") + // // 代码覆盖率 + // branchesTotal Int @map("branches_total") + // branchesCovered Int @map("branches_covered") + // functionsTotal Int @map("functions_total") + // functionsCovered Int @map("functions_covered") + // linesTotal Int @map("lines_total") + // linesCovered Int @map("lines_covered") + // statementsTotal Int @map("statements_total") + // statementsCovered Int @map("statements_covered") + // newlinesTotal Int @map("newlines_total") + // newlinesCovered Int @map("newlines_covered") + // summary String //zstd+pb 必要性,提高概览页面查询速度 + // // 代码覆盖率详情 + // // 覆盖率实体 + // hit String //zstd+pb + // // 通用 + // createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(3) + // + // @@map("ut_coverage") + // } + // id: "1", + sha: "1", + branch: "1", + projectID: "1", + branchesTotal: 1, + branchesCovered: 1, + functionsTotal: 1, + functionsCovered: 1, + linesTotal: 1, + linesCovered: 1, + statementsTotal: 1, + statementsCovered: 1, + newlinesTotal: 1, + newlinesCovered: 1, + summary: "1", + hit: "1", + }, + }); + }), + + getLatest: publicProcedure.query(async ({ ctx }) => { + const coverage = await ctx.db.coverage.findFirst({ + orderBy: { createdAt: "desc" }, + }); + + return coverage ?? null; + }), +}); diff --git a/packages/canyon-ut/src/server/api/trpc.ts b/packages/canyon-ut/src/server/api/trpc.ts new file mode 100644 index 00000000..4e24ba43 --- /dev/null +++ b/packages/canyon-ut/src/server/api/trpc.ts @@ -0,0 +1,106 @@ +/** + * YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS: + * 1. You want to modify request context (see Part 1). + * 2. You want to create a new middleware or type of procedure (see Part 3). + * + * TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will + * need to use are documented accordingly near the end. + */ +import { initTRPC } from "@trpc/server"; +import superjson from "superjson"; +import { ZodError } from "zod"; + +import { db } from "~/server/db"; + +/** + * 1. CONTEXT + * + * This section defines the "contexts" that are available in the backend API. + * + * These allow you to access things when processing a request, like the database, the session, etc. + * + * This helper generates the "internals" for a tRPC context. The API handler and RSC clients each + * wrap this and provides the required context. + * + * @see https://trpc.io/docs/server/context + */ +export const createTRPCContext = async (opts: { headers: Headers }) => { + return { + db, + ...opts, + }; +}; + +/** + * 2. INITIALIZATION + * + * This is where the tRPC API is initialized, connecting the context and transformer. We also parse + * ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation + * errors on the backend. + */ +const t = initTRPC.context().create({ + transformer: superjson, + errorFormatter({ shape, error }) { + return { + ...shape, + data: { + ...shape.data, + zodError: + error.cause instanceof ZodError ? error.cause.flatten() : null, + }, + }; + }, +}); + +/** + * Create a server-side caller. + * + * @see https://trpc.io/docs/server/server-side-calls + */ +export const createCallerFactory = t.createCallerFactory; + +/** + * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT) + * + * These are the pieces you use to build your tRPC API. You should import these a lot in the + * "/src/server/api/routers" directory. + */ + +/** + * This is how you create new routers and sub-routers in your tRPC API. + * + * @see https://trpc.io/docs/router + */ +export const createTRPCRouter = t.router; + +/** + * Middleware for timing procedure execution and adding an artificial delay in development. + * + * You can remove this if you don't like it, but it can help catch unwanted waterfalls by simulating + * network latency that would occur in production but not in local development. + */ +const timingMiddleware = t.middleware(async ({ next, path }) => { + const start = Date.now(); + + if (t._config.isDev) { + // artificial delay in dev + const waitMs = Math.floor(Math.random() * 400) + 100; + await new Promise((resolve) => setTimeout(resolve, waitMs)); + } + + const result = await next(); + + const end = Date.now(); + console.log(`[TRPC] ${path} took ${end - start}ms to execute`); + + return result; +}); + +/** + * Public (unauthenticated) procedure + * + * This is the base piece you use to build new queries and mutations on your tRPC API. It does not + * guarantee that a user querying is authorized, but you can still access user session data if they + * are logged in. + */ +export const publicProcedure = t.procedure.use(timingMiddleware); diff --git a/packages/canyon-ut/src/server/db.ts b/packages/canyon-ut/src/server/db.ts new file mode 100644 index 00000000..07dc0271 --- /dev/null +++ b/packages/canyon-ut/src/server/db.ts @@ -0,0 +1,17 @@ +import { PrismaClient } from "@prisma/client"; + +import { env } from "~/env"; + +const createPrismaClient = () => + new PrismaClient({ + log: + env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"], + }); + +const globalForPrisma = globalThis as unknown as { + prisma: ReturnType | undefined; +}; + +export const db = globalForPrisma.prisma ?? createPrismaClient(); + +if (env.NODE_ENV !== "production") globalForPrisma.prisma = db; diff --git a/packages/canyon-ut/src/styles/globals.css b/packages/canyon-ut/src/styles/globals.css new file mode 100644 index 00000000..b5c61c95 --- /dev/null +++ b/packages/canyon-ut/src/styles/globals.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/packages/canyon-ut/src/trpc/query-client.ts b/packages/canyon-ut/src/trpc/query-client.ts new file mode 100644 index 00000000..bda64397 --- /dev/null +++ b/packages/canyon-ut/src/trpc/query-client.ts @@ -0,0 +1,25 @@ +import { + defaultShouldDehydrateQuery, + QueryClient, +} from "@tanstack/react-query"; +import SuperJSON from "superjson"; + +export const createQueryClient = () => + new QueryClient({ + defaultOptions: { + queries: { + // With SSR, we usually want to set some default staleTime + // above 0 to avoid refetching immediately on the client + staleTime: 30 * 1000, + }, + dehydrate: { + serializeData: SuperJSON.serialize, + shouldDehydrateQuery: (query) => + defaultShouldDehydrateQuery(query) || + query.state.status === "pending", + }, + hydrate: { + deserializeData: SuperJSON.deserialize, + }, + }, + }); diff --git a/packages/canyon-ut/src/trpc/react.tsx b/packages/canyon-ut/src/trpc/react.tsx new file mode 100644 index 00000000..8c0521a7 --- /dev/null +++ b/packages/canyon-ut/src/trpc/react.tsx @@ -0,0 +1,76 @@ +"use client"; + +import { QueryClientProvider, type QueryClient } from "@tanstack/react-query"; +import { loggerLink, unstable_httpBatchStreamLink } from "@trpc/client"; +import { createTRPCReact } from "@trpc/react-query"; +import { type inferRouterInputs, type inferRouterOutputs } from "@trpc/server"; +import { useState } from "react"; +import SuperJSON from "superjson"; + +import { type AppRouter } from "~/server/api/root"; +import { createQueryClient } from "./query-client"; + +let clientQueryClientSingleton: QueryClient | undefined = undefined; +const getQueryClient = () => { + if (typeof window === "undefined") { + // Server: always make a new query client + return createQueryClient(); + } + // Browser: use singleton pattern to keep the same query client + return (clientQueryClientSingleton ??= createQueryClient()); +}; + +export const api = createTRPCReact(); + +/** + * Inference helper for inputs. + * + * @example type HelloInput = RouterInputs['example']['hello'] + */ +export type RouterInputs = inferRouterInputs; + +/** + * Inference helper for outputs. + * + * @example type HelloOutput = RouterOutputs['example']['hello'] + */ +export type RouterOutputs = inferRouterOutputs; + +export function TRPCReactProvider(props: { children: React.ReactNode }) { + const queryClient = getQueryClient(); + + const [trpcClient] = useState(() => + api.createClient({ + links: [ + loggerLink({ + enabled: (op) => + process.env.NODE_ENV === "development" || + (op.direction === "down" && op.result instanceof Error), + }), + unstable_httpBatchStreamLink({ + transformer: SuperJSON, + url: getBaseUrl() + "/api/trpc", + headers: () => { + const headers = new Headers(); + headers.set("x-trpc-source", "nextjs-react"); + return headers; + }, + }), + ], + }) + ); + + return ( + + + {props.children} + + + ); +} + +function getBaseUrl() { + if (typeof window !== "undefined") return window.location.origin; + if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`; + return `http://localhost:${process.env.PORT ?? 3000}`; +} diff --git a/packages/canyon-ut/src/trpc/server.ts b/packages/canyon-ut/src/trpc/server.ts new file mode 100644 index 00000000..59300a63 --- /dev/null +++ b/packages/canyon-ut/src/trpc/server.ts @@ -0,0 +1,30 @@ +import "server-only"; + +import { createHydrationHelpers } from "@trpc/react-query/rsc"; +import { headers } from "next/headers"; +import { cache } from "react"; + +import { createCaller, type AppRouter } from "~/server/api/root"; +import { createTRPCContext } from "~/server/api/trpc"; +import { createQueryClient } from "./query-client"; + +/** + * This wraps the `createTRPCContext` helper and provides the required context for the tRPC API when + * handling a tRPC call from a React Server Component. + */ +const createContext = cache(() => { + const heads = new Headers(headers()); + heads.set("x-trpc-source", "rsc"); + + return createTRPCContext({ + headers: heads, + }); +}); + +const getQueryClient = cache(createQueryClient); +const caller = createCaller(createContext); + +export const { trpc: api, HydrateClient } = createHydrationHelpers( + caller, + getQueryClient +); diff --git a/packages/canyon-ut/tailwind.config.ts b/packages/canyon-ut/tailwind.config.ts new file mode 100644 index 00000000..5fd44e8f --- /dev/null +++ b/packages/canyon-ut/tailwind.config.ts @@ -0,0 +1,14 @@ +import { type Config } from "tailwindcss"; +import { fontFamily } from "tailwindcss/defaultTheme"; + +export default { + content: ["./src/**/*.tsx"], + theme: { + extend: { + fontFamily: { + sans: ["var(--font-geist-sans)", ...fontFamily.sans], + }, + }, + }, + plugins: [], +} satisfies Config; diff --git a/packages/canyon-ut/tsconfig.json b/packages/canyon-ut/tsconfig.json new file mode 100644 index 00000000..905062de --- /dev/null +++ b/packages/canyon-ut/tsconfig.json @@ -0,0 +1,42 @@ +{ + "compilerOptions": { + /* Base Options: */ + "esModuleInterop": true, + "skipLibCheck": true, + "target": "es2022", + "allowJs": true, + "resolveJsonModule": true, + "moduleDetection": "force", + "isolatedModules": true, + + /* Strictness */ + "strict": true, + "noUncheckedIndexedAccess": true, + "checkJs": true, + + /* Bundled projects */ + "lib": ["dom", "dom.iterable", "ES2022"], + "noEmit": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "jsx": "preserve", + "plugins": [{ "name": "next" }], + "incremental": true, + + /* Path Aliases */ + "baseUrl": ".", + "paths": { + "~/*": ["./src/*"] + } + }, + "include": [ + ".eslintrc.cjs", + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + "**/*.cjs", + "**/*.js", + ".next/types/**/*.ts" + ], + "exclude": ["node_modules"] +}