diff --git a/.changeset/witty-seahorses-clean.md b/.changeset/witty-seahorses-clean.md new file mode 100644 index 0000000000..7f768f337c --- /dev/null +++ b/.changeset/witty-seahorses-clean.md @@ -0,0 +1,5 @@ +--- +"create-t3-app": patch +--- + +feat: add oRPC support diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 5c43580b12..0dfd0ccdf2 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -27,40 +27,48 @@ jobs: # contains(github.event.pull_request.labels.*.name, '📌 area: t3-app') strategy: matrix: - trpc: ["true", "false"] tailwind: ["true", "false"] nextAuth: ["true", "false"] prisma: ["true", "false"] appRouter: ["true", "false"] drizzle: ["true", "false"] dbType: ["planetscale", "sqlite", "mysql", "postgres"] - - name: "Build and Start T3 App ${{ matrix.trpc }}-${{ matrix.tailwind }}-${{ matrix.nextAuth }}-${{ matrix.prisma }}-${{ matrix.drizzle}}-${{ matrix.appRouter }}-${{ matrix.dbType }}" + rpc: ["none", "trpc", "orpc"] + exclude: + # prisma and drizzle cannot both be true + - prisma: "true" + drizzle: "true" + # if both are false, dbType must be sqlite + - prisma: "false" + drizzle: "false" + dbType: "planetscale" + - prisma: "false" + drizzle: "false" + dbType: "mysql" + - prisma: "false" + drizzle: "false" + dbType: "postgres" + # orpc only works if appRouter is true + - rpc: "orpc" + appRouter: "false" + + name: "Build and Start T3 App ${{ matrix.tailwind }}-${{ matrix.nextAuth }}-${{ matrix.prisma }}-${{ matrix.drizzle}}-${{ matrix.appRouter }}-${{ matrix.dbType }}-${{ matrix.rpc }}" steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Check valid matrix - id: matrix-valid - run: | - echo "continue=${{ (matrix.prisma == 'false' || matrix.drizzle == 'false') && (matrix.drizzle == 'true' || matrix.prisma == 'true' || matrix.dbType == 'sqlite') }}" >> $GITHUB_OUTPUT - - uses: ./.github/actions/setup - if: ${{ steps.matrix-valid.outputs.continue == 'true' }} - run: pnpm turbo --filter=create-t3-app build - if: ${{ steps.matrix-valid.outputs.continue == 'true' }} # has to be scaffolded outside the CLI project so that no lint/tsconfig are leaking # through. this way it ensures that it is the app's configs that are being used # FIXME: this is a bit hacky, would rather have --packages=trpc,tailwind,... but not sure how to setup the matrix for that - - run: cd cli && pnpm start ../../ci-${{ matrix.trpc }}-${{ matrix.tailwind }}-${{ matrix.nextAuth }}-${{ matrix.prisma }}-${{ matrix.drizzle}}-${{ matrix.appRouter }}-${{ matrix.dbType }} --noGit --CI --trpc=${{ matrix.trpc }} --tailwind=${{ matrix.tailwind }} --nextAuth=${{ matrix.nextAuth }} --prisma=${{ matrix.prisma }} --drizzle=${{ matrix.drizzle }} --appRouter=${{ matrix.appRouter }} --dbProvider=${{ matrix.dbType }} --eslint - if: ${{ steps.matrix-valid.outputs.continue == 'true' }} + - run: cd cli && pnpm start ../../ci-${{ matrix.tailwind }}-${{ matrix.nextAuth }}-${{ matrix.prisma }}-${{ matrix.drizzle}}-${{ matrix.appRouter }}-${{ matrix.dbType }}-${{ matrix.rpc }} --noGit --CI --rpcProvider=${{ matrix.rpc }} --tailwind=${{ matrix.tailwind }} --nextAuth=${{ matrix.nextAuth }} --prisma=${{ matrix.prisma }} --drizzle=${{ matrix.drizzle }} --appRouter=${{ matrix.appRouter }} --dbProvider=${{ matrix.dbType }} --eslint # can't use default mysql string cause t3-env blocks that - - run: cd ../ci-${{ matrix.trpc }}-${{ matrix.tailwind }}-${{ matrix.nextAuth }}-${{ matrix.prisma }}-${{ matrix.drizzle}}-${{ matrix.appRouter }}-${{ matrix.dbType }} && pnpm build - if: ${{ steps.matrix-valid.outputs.continue == 'true' }} + - run: cd ../ci-${{ matrix.tailwind }}-${{ matrix.nextAuth }}-${{ matrix.prisma }}-${{ matrix.drizzle}}-${{ matrix.appRouter }}-${{ matrix.dbType }}-${{ matrix.rpc }} && pnpm build env: AUTH_SECRET: foo AUTH_DISCORD_ID: bar @@ -74,6 +82,13 @@ jobs: matrix: eslint: ["true", "false"] biome: ["true", "false"] + exclude: + # biome and eslint cannot both be true + - eslint: "true" + biome: "true" + # both cannot be false + - eslint: "false" + biome: "false" name: "Build and Start T3 App" steps: @@ -81,22 +96,13 @@ jobs: with: fetch-depth: 0 - - name: Check valid matrix - id: matrix-valid - run: | - echo "continue=${{ (matrix.eslint == 'false' || matrix.biome == 'false') && (matrix.biome == 'true' || matrix.eslint == 'true') }}" >> $GITHUB_OUTPUT - - uses: ./.github/actions/setup - if: ${{ steps.matrix-valid.outputs.continue == 'true' }} - run: pnpm turbo --filter=create-t3-app build - if: ${{ steps.matrix-valid.outputs.continue == 'true' }} - - run: cd cli && pnpm start ../../ci-format-${{ matrix.eslint }}-${{ matrix.biome }} --noGit --CI --trpc --tailwind --nextAuth --drizzle --appRouter --dbProvider=postgres --eslint=${{ matrix.eslint }} --biome=${{ matrix.biome }} - if: ${{ steps.matrix-valid.outputs.continue == 'true' }} + - run: cd cli && pnpm start ../../ci-format-${{ matrix.eslint }}-${{ matrix.biome }} --noGit --CI --rpcProvider=trpc --tailwind --nextAuth --drizzle --appRouter --dbProvider=postgres --eslint=${{ matrix.eslint }} --biome=${{ matrix.biome }} - run: cd ../ci-format-${{ matrix.eslint }}-${{ matrix.biome }} && pnpm build - if: ${{ steps.matrix-valid.outputs.continue == 'true' }} env: AUTH_SECRET: foo AUTH_DISCORD_ID: bar @@ -105,11 +111,11 @@ jobs: # Run biome check - run: cd ../ci-format-${{ matrix.eslint }}-${{ matrix.biome }} && pnpm check - if: ${{ steps.matrix-valid.outputs.continue == 'true' && matrix.biome == 'true' }} + if: ${{ matrix.biome == 'true' }} # Check linting and formatting with eslint and prettier - run: cd ../ci-format-${{ matrix.eslint }}-${{ matrix.biome }} && pnpm lint && pnpm format:check - if: ${{ steps.matrix-valid.outputs.continue == 'true' && matrix.eslint == 'true' }} + if: ${{ matrix.eslint == 'true' }} env: AUTH_SECRET: foo AUTH_DISCORD_ID: bar diff --git a/cli/package.json b/cli/package.json index cc849a3819..ca80292064 100644 --- a/cli/package.json +++ b/cli/package.json @@ -65,12 +65,16 @@ "@auth/drizzle-adapter": "^1.1.0", "@auth/prisma-adapter": "^1.6.0", "@libsql/client": "^0.14.0", + "@orpc/client": "^0.53.0", + "@orpc/react-query": "^0.53.0", + "@orpc/server": "^0.53.0", "@planetscale/database": "^1.19.0", "@prisma/adapter-planetscale": "^6.6.0", "@prisma/client": "^6.6.0", "@t3-oss/env-nextjs": "^0.12.0", "@tailwindcss/postcss": "^4.0.15", - "@tanstack/react-query": "^5.69.0", + "@tanstack/react-query": "^5.72.1", + "@tanstack/react-query-next-experimental": "^5.72.1", "@trpc/client": "11.0.0", "@trpc/next": "11.0.0", "@trpc/react-query": "11.0.0", diff --git a/cli/src/cli/index.ts b/cli/src/cli/index.ts index 013c4c36ea..b3e31d7ab6 100644 --- a/cli/src/cli/index.ts +++ b/cli/src/cli/index.ts @@ -5,8 +5,10 @@ import { Command } from "commander"; import { CREATE_T3_APP, DEFAULT_APP_NAME } from "~/consts.js"; import { databaseProviders, + rpcProviders, type AvailablePackages, type DatabaseProvider, + type RpcProvider, } from "~/installers/index.js"; import { getVersion } from "~/utils/getT3Version.js"; import { getUserPkgManager } from "~/utils/getUserPkgManager.js"; @@ -26,8 +28,6 @@ interface CliFlags { /** @internal Used in CI. */ tailwind: boolean; /** @internal Used in CI. */ - trpc: boolean; - /** @internal Used in CI. */ prisma: boolean; /** @internal Used in CI. */ drizzle: boolean; @@ -38,6 +38,8 @@ interface CliFlags { /** @internal Used in CI. */ dbProvider: DatabaseProvider; /** @internal Used in CI */ + rpcProvider: RpcProvider; + /** @internal Used in CI */ eslint: boolean; /** @internal Used in CI */ biome: boolean; @@ -59,13 +61,13 @@ const defaultOptions: CliResults = { default: false, CI: false, tailwind: false, - trpc: false, prisma: false, drizzle: false, nextAuth: false, importAlias: "~/", appRouter: false, dbProvider: "sqlite", + rpcProvider: "none", eslint: false, biome: false, }, @@ -129,9 +131,11 @@ export const runCli = async (): Promise => { ) /** @experimental - Used for CI E2E tests. Used in conjunction with `--CI` to skip prompting. */ .option( - "--trpc [boolean]", - "Experimental: Boolean value if we should install tRPC. Must be used in conjunction with `--CI`.", - (value) => !!value && value !== "false" + "--rpcProvider [provider]", + `Experimental: Choose a RPC provider to use. Possible values: ${rpcProviders.join( + ", " + )}`, + defaultOptions.flags.rpcProvider ) /** @experimental - Used for CI E2E tests. Used in conjunction with `--CI` to skip prompting. */ .option( @@ -194,7 +198,6 @@ export const runCli = async (): Promise => { /** @internal Used for CI E2E tests. */ if (cliResults.flags.CI) { cliResults.packages = []; - if (cliResults.flags.trpc) cliResults.packages.push("trpc"); if (cliResults.flags.tailwind) cliResults.packages.push("tailwind"); if (cliResults.flags.prisma) cliResults.packages.push("prisma"); if (cliResults.flags.drizzle) cliResults.packages.push("drizzle"); @@ -219,6 +222,23 @@ export const runCli = async (): Promise => { process.exit(0); } + if (rpcProviders.includes(cliResults.flags.rpcProvider) === false) { + logger.warn( + `Incompatible RPC provider. Use: ${rpcProviders.join(", ")}. Exiting.` + ); + process.exit(0); + } else if ( + !cliResults.flags.appRouter && + cliResults.flags.rpcProvider === "orpc" + ) { + logger.warn( + `Incompatible RPC provider. oRPC requires the App Router. Exiting.` + ); + process.exit(0); + } else if (cliResults.flags.rpcProvider !== "none") { + cliResults.packages.push(cliResults.flags.rpcProvider); + } + cliResults.databaseProvider = cliResults.packages.includes("drizzle") || cliResults.packages.includes("prisma") @@ -275,11 +295,6 @@ export const runCli = async (): Promise => { message: "Will you be using Tailwind CSS for styling?", }); }, - trpc: () => { - return p.confirm({ - message: "Would you like to use tRPC?", - }); - }, authentication: () => { return p.select({ message: "What authentication provider would you like to use?", @@ -309,6 +324,17 @@ export const runCli = async (): Promise => { initialValue: true, }); }, + rpc: ({ results }) => { + return p.select({ + message: "Which RPC provider would you like to use?", + options: [ + { value: "none", label: "None" }, + { value: "trpc", label: "tRPC" }, + ...(results.appRouter ? [{ value: "orpc", label: "oRPC" }] : []), + ], + initialValue: "none", + }); + }, databaseProvider: ({ results }) => { if (results.database === "none") return; return p.select({ @@ -370,7 +396,8 @@ export const runCli = async (): Promise => { const packages: AvailablePackages[] = []; if (project.styling) packages.push("tailwind"); - if (project.trpc) packages.push("trpc"); + if (project.rpc === "trpc") packages.push("trpc"); + if (project.rpc === "orpc") packages.push("orpc"); if (project.authentication === "next-auth") packages.push("nextAuth"); if (project.database === "prisma") packages.push("prisma"); if (project.database === "drizzle") packages.push("drizzle"); diff --git a/cli/src/helpers/selectBoilerplate.ts b/cli/src/helpers/selectBoilerplate.ts index 2b07079be6..0c8d688188 100644 --- a/cli/src/helpers/selectBoilerplate.ts +++ b/cli/src/helpers/selectBoilerplate.ts @@ -49,12 +49,17 @@ export const selectLayoutFile = ({ const usingTw = packages.tailwind.inUse; const usingTRPC = packages.trpc.inUse; + const usingORPC = packages.orpc.inUse; let layoutFile = "base.tsx"; if (usingTRPC && usingTw) { layoutFile = "with-trpc-tw.tsx"; - } else if (usingTRPC && !usingTw) { + } else if (usingTRPC) { layoutFile = "with-trpc.tsx"; - } else if (!usingTRPC && usingTw) { + } else if (usingORPC && usingTw) { + layoutFile = "with-orpc-tw.tsx"; + } else if (usingORPC) { + layoutFile = "with-orpc.tsx"; + } else if (usingTw) { layoutFile = "with-tw.tsx"; } @@ -100,6 +105,7 @@ export const selectPageFile = ({ const indexFileDir = path.join(PKG_ROOT, "template/extras/src/app/page"); const usingTRPC = packages.trpc.inUse; + const usingORPC = packages.orpc.inUse; const usingTw = packages.tailwind.inUse; const usingAuth = packages.nextAuth.inUse; @@ -112,7 +118,15 @@ export const selectPageFile = ({ indexFile = "with-trpc-tw.tsx"; } else if (usingTRPC && !usingTw) { indexFile = "with-trpc.tsx"; - } else if (!usingTRPC && usingTw) { + } else if (usingORPC && usingTw && usingAuth) { + indexFile = "with-auth-orpc-tw.tsx"; + } else if (usingORPC && !usingTw && usingAuth) { + indexFile = "with-auth-orpc.tsx"; + } else if (usingORPC && usingTw) { + indexFile = "with-orpc-tw.tsx"; + } else if (usingORPC && !usingTw) { + indexFile = "with-orpc.tsx"; + } else if (usingTw) { indexFile = "with-tw.tsx"; } diff --git a/cli/src/installers/dependencyVersionMap.ts b/cli/src/installers/dependencyVersionMap.ts index a346468c81..a95f522443 100644 --- a/cli/src/installers/dependencyVersionMap.ts +++ b/cli/src/installers/dependencyVersionMap.ts @@ -31,10 +31,18 @@ export const dependencyVersionMap = { "@trpc/server": "^11.0.0", "@trpc/react-query": "^11.0.0", "@trpc/next": "^11.0.0", - "@tanstack/react-query": "^5.69.0", superjson: "^2.2.1", "server-only": "^0.0.1", + // oRPC + "@orpc/client": "^1.1.1", + "@orpc/react-query": "^1.1.1", + "@orpc/server": "^1.1.1", + + // Tanstack Query + "@tanstack/react-query": "^5.72.1", + "@tanstack/react-query-next-experimental": "^5.72.1", + // biome "@biomejs/biome": "1.9.4", diff --git a/cli/src/installers/index.ts b/cli/src/installers/index.ts index e04718c532..2b1b837635 100644 --- a/cli/src/installers/index.ts +++ b/cli/src/installers/index.ts @@ -8,6 +8,7 @@ import { biomeInstaller } from "./biome.js"; import { dbContainerInstaller } from "./dbContainer.js"; import { drizzleInstaller } from "./drizzle.js"; import { dynamicEslintInstaller } from "./eslint.js"; +import { orpcInstaller } from "./orpc.js"; // Turning this into a const allows the list to be iterated over for programmatically creating prompt options // Should increase extensibility in the future @@ -17,6 +18,7 @@ export const availablePackages = [ "drizzle", "tailwind", "trpc", + "orpc", "envVariables", "eslint", "biome", @@ -32,6 +34,9 @@ export const databaseProviders = [ ] as const; export type DatabaseProvider = (typeof databaseProviders)[number]; +export const rpcProviders = ["none", "trpc", "orpc"] as const; +export type RpcProvider = (typeof rpcProviders)[number]; + export interface InstallerOptions { projectDir: string; pkgManager: PackageManager; @@ -77,6 +82,10 @@ export const buildPkgInstallerMap = ( inUse: packages.includes("trpc"), installer: trpcInstaller, }, + orpc: { + inUse: packages.includes("orpc"), + installer: orpcInstaller, + }, dbContainer: { inUse: ["mysql", "postgres"].includes(databaseProvider), installer: dbContainerInstaller, diff --git a/cli/src/installers/orpc.ts b/cli/src/installers/orpc.ts new file mode 100644 index 0000000000..72c4ceb970 --- /dev/null +++ b/cli/src/installers/orpc.ts @@ -0,0 +1,110 @@ +import path from "path"; +import fs from "fs-extra"; + +import { PKG_ROOT } from "~/consts.js"; +import { type Installer } from "~/installers/index.js"; +import { addPackageDependency } from "~/utils/addPackageDependency.js"; + +export const orpcInstaller: Installer = ({ projectDir, packages }) => { + addPackageDependency({ + projectDir, + dependencies: [ + "@tanstack/react-query", + "@tanstack/react-query-next-experimental", + "@orpc/server", + "@orpc/client", + "@orpc/react-query", + ], + devMode: false, + }); + + const usingAuth = packages?.nextAuth.inUse; + const usingPrisma = packages?.prisma.inUse; + const usingDrizzle = packages?.drizzle.inUse; + const usingDb = usingPrisma === true || usingDrizzle === true; + + const extrasDir = path.join(PKG_ROOT, "template/extras"); + + const routeHandlerFile = "src/app/api/orpc/[[...rest]]/route.ts"; + + const apiHandlerSrc = path.join(extrasDir, routeHandlerFile); + const apiHandlerDest = path.join(projectDir, routeHandlerFile); + + const orpcFile = + usingAuth && usingDb + ? "with-auth-db.ts" + : usingAuth + ? "with-auth.ts" + : usingDb + ? "with-db.ts" + : "base.ts"; + const orpcSrc = path.join(extrasDir, "src/server/api", "orpc-app", orpcFile); + const orpcDest = path.join(projectDir, "src/server/api/procedures.ts"); + + const rootRouterSrc = path.join(extrasDir, "src/server/api/orpc-index.ts"); + const rootRouterDest = path.join(projectDir, "src/server/api/index.ts"); + + const providerFileSrc = path.join( + extrasDir, + "src/app/providers/with-orpc.tsx" + ); + const providerFileDest = path.join(projectDir, "src/app/providers.tsx"); + + const exampleRouterFile = + usingAuth && usingPrisma + ? "with-auth-prisma.ts" + : usingAuth && usingDrizzle + ? "with-auth-drizzle.ts" + : usingAuth + ? "with-auth.ts" + : usingPrisma + ? "with-prisma.ts" + : usingDrizzle + ? "with-drizzle.ts" + : "base.ts"; + + const exampleRouterSrc = path.join( + extrasDir, + "src/server/api/routers/orpc-post", + exampleRouterFile + ); + const exampleRouterDest = path.join( + projectDir, + "src/server/api/routers/post.ts" + ); + + const copySrcDest: [string, string][] = [ + [apiHandlerSrc, apiHandlerDest], + [orpcSrc, orpcDest], + [rootRouterSrc, rootRouterDest], + [providerFileSrc, providerFileDest], + [exampleRouterSrc, exampleRouterDest], + ]; + + const orpcDir = path.join(extrasDir, "src/orpc"); + copySrcDest.push( + [ + path.join(orpcDir, "client.ts"), + path.join(projectDir, "src/orpc/client.ts"), + ], + [ + path.join(orpcDir, "context.ts"), + path.join(projectDir, "src/orpc/context.ts"), + ], + [ + path.join(orpcDir, "query-client.ts"), + path.join(projectDir, "src/orpc/query-client.ts"), + ], + [ + path.join( + extrasDir, + "src/app/_components", + packages?.tailwind.inUse ? "post-orpc-tw.tsx" : "post-orpc.tsx" + ), + path.join(projectDir, "src/app/_components/post.tsx"), + ] + ); + copySrcDest.forEach(([src, dest]) => { + fs.copySync(src, dest); + }); +}; diff --git a/cli/src/installers/trpc.ts b/cli/src/installers/trpc.ts index 697867a662..81be5d075e 100644 --- a/cli/src/installers/trpc.ts +++ b/cli/src/installers/trpc.ts @@ -70,7 +70,7 @@ export const trpcInstaller: Installer = ({ const exampleRouterSrc = path.join( extrasDir, - "src/server/api/routers/post", + "src/server/api/routers/trpc-post", exampleRouterFile ); const exampleRouterDest = path.join( @@ -106,7 +106,7 @@ export const trpcInstaller: Installer = ({ path.join( extrasDir, "src/app/_components", - packages?.tailwind.inUse ? "post-tw.tsx" : "post.tsx" + packages?.tailwind.inUse ? "post-trpc-tw.tsx" : "post-trpc.tsx" ), path.join(projectDir, "src/app/_components/post.tsx"), ], diff --git a/cli/template/extras/src/app/_components/post-orpc-tw.tsx b/cli/template/extras/src/app/_components/post-orpc-tw.tsx new file mode 100644 index 0000000000..3cba5ec9d5 --- /dev/null +++ b/cli/template/extras/src/app/_components/post-orpc-tw.tsx @@ -0,0 +1,55 @@ +"use client"; + +import { useMutation, useSuspenseQuery } from "@tanstack/react-query"; +import { useState } from "react"; + +import { useORPC } from "~/orpc/context"; + +export function LatestPost() { + const [name, setName] = useState(""); + const orpc = useORPC(); + + const latestPost = useSuspenseQuery(orpc.post.getLatest.queryOptions()); + const createPost = useMutation( + orpc.post.create.mutationOptions({ + onSuccess() { + void latestPost.refetch(); + setName(""); + }, + }) + ); + + return ( +
+ {latestPost.data ? ( +

+ Your most recent post: {latestPost.data.name} +

+ ) : ( +

You have no posts yet.

+ )} +
{ + e.preventDefault(); + createPost.mutate({ name }); + }} + className="flex flex-col gap-2" + > + setName(e.target.value)} + className="w-full rounded-full bg-white/10 px-4 py-2 text-white" + /> + +
+
+ ); +} diff --git a/cli/template/extras/src/app/_components/post-orpc.tsx b/cli/template/extras/src/app/_components/post-orpc.tsx new file mode 100644 index 0000000000..822d6b16e8 --- /dev/null +++ b/cli/template/extras/src/app/_components/post-orpc.tsx @@ -0,0 +1,56 @@ +"use client"; + +import { useMutation, useSuspenseQuery } from "@tanstack/react-query"; +import { useState } from "react"; + +import { useORPC } from "~/orpc/context"; +import styles from "../index.module.css"; + +export function LatestPost() { + const [name, setName] = useState(""); + const orpc = useORPC(); + + const latestPost = useSuspenseQuery(orpc.post.getLatest.queryOptions()); + const createPost = useMutation( + orpc.post.create.mutationOptions({ + onSuccess() { + void latestPost.refetch(); + setName(""); + }, + }) + ); + + return ( +
+ {latestPost.data ? ( +

+ Your most recent post: {latestPost.data.name} +

+ ) : ( +

You have no posts yet.

+ )} +
{ + e.preventDefault(); + createPost.mutate({ name }); + }} + className={styles.form} + > + setName(e.target.value)} + className={styles.input} + /> + +
+
+ ); +} diff --git a/cli/template/extras/src/app/_components/post-tw.tsx b/cli/template/extras/src/app/_components/post-trpc-tw.tsx similarity index 100% rename from cli/template/extras/src/app/_components/post-tw.tsx rename to cli/template/extras/src/app/_components/post-trpc-tw.tsx diff --git a/cli/template/extras/src/app/_components/post.tsx b/cli/template/extras/src/app/_components/post-trpc.tsx similarity index 100% rename from cli/template/extras/src/app/_components/post.tsx rename to cli/template/extras/src/app/_components/post-trpc.tsx diff --git a/cli/template/extras/src/app/api/orpc/[[...rest]]/route.ts b/cli/template/extras/src/app/api/orpc/[[...rest]]/route.ts new file mode 100644 index 0000000000..fd112f6134 --- /dev/null +++ b/cli/template/extras/src/app/api/orpc/[[...rest]]/route.ts @@ -0,0 +1,16 @@ +import { handler } from "~/server/api"; + +async function handleRequest(request: Request) { + const { response } = await handler.handle(request, { + prefix: "/api/orpc", + context: {}, + }); + + return response ?? new Response("Not found", { status: 404 }); +} + +export const GET = handleRequest; +export const POST = handleRequest; +export const PUT = handleRequest; +export const PATCH = handleRequest; +export const DELETE = handleRequest; diff --git a/cli/template/extras/src/app/layout/with-orpc-tw.tsx b/cli/template/extras/src/app/layout/with-orpc-tw.tsx new file mode 100644 index 0000000000..020e238db1 --- /dev/null +++ b/cli/template/extras/src/app/layout/with-orpc-tw.tsx @@ -0,0 +1,32 @@ +import "~/styles/globals.css"; + +import { type Metadata } from "next"; +import { Geist } from "next/font/google"; +import { headers } from "next/headers"; + +import Providers from "./providers"; + +export const metadata: Metadata = { + title: "Create T3 App", + description: "Generated by create-t3-app", + icons: [{ rel: "icon", url: "/favicon.ico" }], +}; + +const geist = Geist({ + subsets: ["latin"], + variable: "--font-geist-sans", +}); + +export default async function RootLayout({ + children, +}: Readonly<{ children: React.ReactNode }>) { + const cookie = (await headers()).get("Cookie") ?? undefined; + + return ( + + + {children} + + + ); +} diff --git a/cli/template/extras/src/app/layout/with-orpc.tsx b/cli/template/extras/src/app/layout/with-orpc.tsx new file mode 100644 index 0000000000..6a664cd11f --- /dev/null +++ b/cli/template/extras/src/app/layout/with-orpc.tsx @@ -0,0 +1,31 @@ +import "~/styles/globals.css"; + +import { type Metadata } from "next"; +import { Geist } from "next/font/google"; +import { headers } from "next/headers"; + +import Providers from "./providers"; + +export const metadata: Metadata = { + title: "Create T3 App", + description: "Generated by create-t3-app", + icons: [{ rel: "icon", url: "/favicon.ico" }], +}; + +const geist = Geist({ + subsets: ["latin"], +}); + +export default async function RootLayout({ + children, +}: Readonly<{ children: React.ReactNode }>) { + const cookie = (await headers()).get("Cookie") ?? undefined; + + return ( + + + {children} + + + ); +} diff --git a/cli/template/extras/src/app/page/with-auth-orpc-tw.tsx b/cli/template/extras/src/app/page/with-auth-orpc-tw.tsx new file mode 100644 index 0000000000..d3ec044698 --- /dev/null +++ b/cli/template/extras/src/app/page/with-auth-orpc-tw.tsx @@ -0,0 +1,68 @@ +import Link from "next/link"; + +import { LatestPost } from "~/app/_components/post"; +import { api } from "~/server/api"; +import { auth } from "~/server/auth"; + +export default async function Home() { + const hello = await api.post.hello({ text: "from oRPC" }); + const session = await auth(); + + let secretMessage; + if (session) { + secretMessage = await api.post.getSecretMessage(); + } + + 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..."} +

+

{secretMessage}

+
+

+ {session && Logged in as {session.user?.name}} +

+ + {session ? "Sign out" : "Sign in"} + +
+
+ + {session?.user && } +
+
+ ); +} diff --git a/cli/template/extras/src/app/page/with-auth-orpc.tsx b/cli/template/extras/src/app/page/with-auth-orpc.tsx new file mode 100644 index 0000000000..484bc94a53 --- /dev/null +++ b/cli/template/extras/src/app/page/with-auth-orpc.tsx @@ -0,0 +1,69 @@ +import Link from "next/link"; + +import { LatestPost } from "~/app/_components/post"; +import { api } from "~/server/api"; +import { auth } from "~/server/auth"; +import styles from "./index.module.css"; + +export default async function Home() { + const hello = await api.post.hello({ text: "from oRPC" }); + const session = await auth(); + + let secretMessage; + if (session) { + secretMessage = await api.post.getSecretMessage(); + } + + 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..."} +

+

{secretMessage}

+
+

+ {session && Logged in as {session.user?.name}} +

+ + {session ? "Sign out" : "Sign in"} + +
+
+ + {session?.user && } +
+
+ ); +} diff --git a/cli/template/extras/src/app/page/with-orpc-tw.tsx b/cli/template/extras/src/app/page/with-orpc-tw.tsx new file mode 100644 index 0000000000..b2de312e92 --- /dev/null +++ b/cli/template/extras/src/app/page/with-orpc-tw.tsx @@ -0,0 +1,49 @@ +import Link from "next/link"; + +import { LatestPost } from "~/app/_components/post"; +import { api } from "~/server/api"; + +export default async function Home() { + const hello = await api.post.hello({ text: "from oRPC" }); + + 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/cli/template/extras/src/app/page/with-orpc.tsx b/cli/template/extras/src/app/page/with-orpc.tsx new file mode 100644 index 0000000000..92f6106f33 --- /dev/null +++ b/cli/template/extras/src/app/page/with-orpc.tsx @@ -0,0 +1,50 @@ +import Link from "next/link"; + +import { LatestPost } from "~/app/_components/post"; +import { api } from "~/server/api"; +import styles from "./index.module.css"; + +export default async function Home() { + const hello = await api.post.hello({ text: "from oRPC" }); + + 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/cli/template/extras/src/app/providers/with-orpc.tsx b/cli/template/extras/src/app/providers/with-orpc.tsx new file mode 100644 index 0000000000..adb777cb78 --- /dev/null +++ b/cli/template/extras/src/app/providers/with-orpc.tsx @@ -0,0 +1,27 @@ +"use client"; + +import { QueryClientProvider } from "@tanstack/react-query"; +import { ReactQueryStreamedHydration } from "@tanstack/react-query-next-experimental"; + +import { createORPC } from "~/orpc/client"; +import { ORPCContext } from "~/orpc/context"; +import { getQueryClient } from "~/orpc/query-client"; + +export default function Providers({ + children, + cookie, +}: { + children: React.ReactNode; + cookie?: string; +}) { + const queryClient = getQueryClient(); + const orpc = createORPC(cookie); + + return ( + + + {children} + + + ); +} diff --git a/cli/template/extras/src/orpc/client.ts b/cli/template/extras/src/orpc/client.ts new file mode 100644 index 0000000000..8c9e43ba4f --- /dev/null +++ b/cli/template/extras/src/orpc/client.ts @@ -0,0 +1,36 @@ +import { createORPCClient } from "@orpc/client"; +import { RPCLink } from "@orpc/client/fetch"; +import { BatchLinkPlugin } from "@orpc/client/plugins"; +import { createORPCReactQueryUtils } from "@orpc/react-query"; +import { type RouterClient } from "@orpc/server"; + +import { type ORPCRouter } from "~/server/api"; + +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}`; +} + +export function createORPC(cookie?: string) { + const link = new RPCLink({ + url: getBaseUrl() + "/api/orpc", + headers: { + Cookie: cookie, + }, + plugins: [ + new BatchLinkPlugin({ + groups: [ + { + condition: () => true, + context: {}, + }, + ], + }), + ], + }); + + const client: RouterClient = createORPCClient(link); + + return createORPCReactQueryUtils(client); +} diff --git a/cli/template/extras/src/orpc/context.ts b/cli/template/extras/src/orpc/context.ts new file mode 100644 index 0000000000..39cd8f3630 --- /dev/null +++ b/cli/template/extras/src/orpc/context.ts @@ -0,0 +1,17 @@ +import { type RouterUtils } from "@orpc/react-query"; +import { type RouterClient } from "@orpc/server"; +import { createContext, useContext } from "react"; + +import { type ORPCRouter } from "~/server/api"; + +type ORPCReactUtils = RouterUtils>; + +export const ORPCContext = createContext(undefined); + +export function useORPC(): ORPCReactUtils { + const orpc = useContext(ORPCContext); + if (!orpc) { + throw new Error("ORPCContext is not set up properly"); + } + return orpc; +} diff --git a/cli/template/extras/src/orpc/query-client.ts b/cli/template/extras/src/orpc/query-client.ts new file mode 100644 index 0000000000..65a897f1fe --- /dev/null +++ b/cli/template/extras/src/orpc/query-client.ts @@ -0,0 +1,29 @@ +import { isServer, QueryClient } from "@tanstack/react-query"; + +function makeQueryClient() { + return new QueryClient({ + defaultOptions: { + queries: { + // With SSR, we usually want to set some default staleTime + // above 0 to avoid refetching immediately on the client + staleTime: 60 * 1000, + }, + }, + }); +} + +let browserQueryClient: QueryClient | undefined = undefined; + +export function getQueryClient() { + if (isServer) { + // Server: always make a new query client + return makeQueryClient(); + } else { + // Browser: make a new query client if we don't already have one + // This is very important, so we don't re-make a new client if React + // suspends during the initial render. This may not be needed if we + // have a suspense boundary BELOW the creation of the query client + browserQueryClient ??= makeQueryClient(); + return browserQueryClient; + } +} diff --git a/cli/template/extras/src/server/api/orpc-app/base.ts b/cli/template/extras/src/server/api/orpc-app/base.ts new file mode 100644 index 0000000000..827cbaaa0e --- /dev/null +++ b/cli/template/extras/src/server/api/orpc-app/base.ts @@ -0,0 +1,35 @@ +import { os } from "@orpc/server"; + +import { env } from "~/env"; + +/** + * 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 = os.middleware(async ({ next, path }) => { + const start = Date.now(); + + if (env.NODE_ENV === "development") { + // 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(`[ORPC] ${path.join(".")} 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 = os.use(timingMiddleware); diff --git a/cli/template/extras/src/server/api/orpc-app/with-auth-db.ts b/cli/template/extras/src/server/api/orpc-app/with-auth-db.ts new file mode 100644 index 0000000000..b15c229e2e --- /dev/null +++ b/cli/template/extras/src/server/api/orpc-app/with-auth-db.ts @@ -0,0 +1,75 @@ +import { ORPCError, os } from "@orpc/server"; + +import { env } from "~/env"; +import { auth } from "~/server/auth"; +import { db } from "~/server/db"; + +/** + * 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 = os.middleware(async ({ next, path }) => { + const start = Date.now(); + + if (env.NODE_ENV === "development") { + // 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(`[ORPC] ${path.join(".")} 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 = os + .use(timingMiddleware) + .use(async ({ next }) => { + const session = await auth(); + + const result = await next({ + context: { + session, + db, + }, + }); + + return result; + }); + +/** + * Protected (authenticated) procedure + * + * If you want a query or mutation to ONLY be accessible to logged in users, use this. It verifies + * the session is valid and guarantees `context.session` is not null. + */ +export const protectedProcedure = publicProcedure.use( + async ({ context, next }) => { + const session = context.session; + if (!session) { + throw new ORPCError("UNAUTHORIZED", { + message: "You must be logged in to access this resource.", + }); + } + + const result = await next({ + context: { + ...context, + session: session, + }, + }); + return result; + } +); diff --git a/cli/template/extras/src/server/api/orpc-app/with-auth.ts b/cli/template/extras/src/server/api/orpc-app/with-auth.ts new file mode 100644 index 0000000000..00f7263110 --- /dev/null +++ b/cli/template/extras/src/server/api/orpc-app/with-auth.ts @@ -0,0 +1,73 @@ +import { ORPCError, os } from "@orpc/server"; + +import { env } from "~/env"; +import { auth } from "~/server/auth"; + +/** + * 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 = os.middleware(async ({ next, path }) => { + const start = Date.now(); + + if (env.NODE_ENV === "development") { + // 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(`[ORPC] ${path.join(".")} 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 = os + .use(timingMiddleware) + .use(async ({ next }) => { + const session = await auth(); + + const result = await next({ + context: { + session, + }, + }); + + return result; + }); + +/** + * Protected (authenticated) procedure + * + * If you want a query or mutation to ONLY be accessible to logged in users, use this. It verifies + * the session is valid and guarantees `context.session` is not null. + */ +export const protectedProcedure = publicProcedure.use( + async ({ context, next }) => { + const session = context.session; + if (!session) { + throw new ORPCError("UNAUTHORIZED", { + message: "You must be logged in to access this resource.", + }); + } + + const result = await next({ + context: { + ...context, + session: session, + }, + }); + return result; + } +); diff --git a/cli/template/extras/src/server/api/orpc-app/with-db.ts b/cli/template/extras/src/server/api/orpc-app/with-db.ts new file mode 100644 index 0000000000..edd85ecc5c --- /dev/null +++ b/cli/template/extras/src/server/api/orpc-app/with-db.ts @@ -0,0 +1,46 @@ +import { os } from "@orpc/server"; + +import { env } from "~/env"; +import { db } from "~/server/db"; + +/** + * 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 = os.middleware(async ({ next, path }) => { + const start = Date.now(); + + if (env.NODE_ENV === "development") { + // 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(`[ORPC] ${path.join(".")} 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 = os + .use(timingMiddleware) + .use(async ({ next }) => { + const result = await next({ + context: { + db, + }, + }); + + return result; + }); diff --git a/cli/template/extras/src/server/api/orpc-index.ts b/cli/template/extras/src/server/api/orpc-index.ts new file mode 100644 index 0000000000..bea50bee1c --- /dev/null +++ b/cli/template/extras/src/server/api/orpc-index.ts @@ -0,0 +1,32 @@ +import { createRouterClient } from "@orpc/server"; +import { RPCHandler } from "@orpc/server/fetch"; +import { BatchHandlerPlugin } from "@orpc/server/plugins"; + +import { postRouter } from "./routers/post"; + +/** + * This is the primary router for your server. + * + * All routers added in /api/routers should be manually added here. + */ +const router = { + post: postRouter, +}; + +// export type definition of router +export type ORPCRouter = typeof router; + +/** + * Export handler for next app router + */ +export const handler = new RPCHandler(router, { + plugins: [new BatchHandlerPlugin()], +}); + +/** + * Export a server-side caller for the oRPC API. + * @example + * const res = await api.post.all(); + * ^? Post[] + */ +export const api = createRouterClient(router); diff --git a/cli/template/extras/src/server/api/routers/orpc-post/base.ts b/cli/template/extras/src/server/api/routers/orpc-post/base.ts new file mode 100644 index 0000000000..7fd0b9e34f --- /dev/null +++ b/cli/template/extras/src/server/api/routers/orpc-post/base.ts @@ -0,0 +1,44 @@ +import { z } from "zod"; + +import { publicProcedure } from "~/server/api/procedures"; + +// Mocked DB +interface Post { + id: number; + name: string; +} +const posts: Post[] = [ + { + id: 1, + name: "Hello World", + }, +]; + +const hello = publicProcedure + .input(z.object({ text: z.string() })) + .handler(({ input }) => { + return { + greeting: `Hello ${input.text}`, + }; + }); + +const create = publicProcedure + .input(z.object({ name: z.string().min(1) })) + .handler(async ({ input }) => { + const post: Post = { + id: posts.length + 1, + name: input.name, + }; + posts.push(post); + return post; + }); + +const getLatest = publicProcedure.handler(() => { + return posts.at(-1) ?? null; +}); + +export const postRouter = { + hello, + create, + getLatest, +}; diff --git a/cli/template/extras/src/server/api/routers/orpc-post/with-auth-drizzle.ts b/cli/template/extras/src/server/api/routers/orpc-post/with-auth-drizzle.ts new file mode 100644 index 0000000000..1f39f7a896 --- /dev/null +++ b/cli/template/extras/src/server/api/routers/orpc-post/with-auth-drizzle.ts @@ -0,0 +1,40 @@ +import { z } from "zod"; + +import { protectedProcedure, publicProcedure } from "~/server/api/procedures"; +import { posts } from "~/server/db/schema"; + +const hello = publicProcedure + .input(z.object({ text: z.string() })) + .handler(({ input }) => { + return { + greeting: `Hello ${input.text}`, + }; + }); + +const create = protectedProcedure + .input(z.object({ name: z.string().min(1) })) + .handler(async ({ context, input }) => { + return await context.db.insert(posts).values({ + name: input.name, + createdById: context.session.user.id, + }); + }); + +const getLatest = protectedProcedure.handler(async ({ context }) => { + const post = await context.db.query.posts.findFirst({ + orderBy: (posts, { desc }) => [desc(posts.createdAt)], + where: (posts, { eq }) => eq(posts.createdById, context.session.user.id), + }); + return post ?? null; +}); + +const getSecretMessage = protectedProcedure.handler(() => { + return "you can now see this secret message!"; +}); + +export const postRouter = { + hello, + create, + getLatest, + getSecretMessage, +}; diff --git a/cli/template/extras/src/server/api/routers/orpc-post/with-auth-prisma.ts b/cli/template/extras/src/server/api/routers/orpc-post/with-auth-prisma.ts new file mode 100644 index 0000000000..3a0c1f7569 --- /dev/null +++ b/cli/template/extras/src/server/api/routers/orpc-post/with-auth-prisma.ts @@ -0,0 +1,41 @@ +import { z } from "zod"; + +import { protectedProcedure, publicProcedure } from "~/server/api/procedures"; + +const hello = publicProcedure + .input(z.object({ text: z.string() })) + .handler(({ input }) => { + return { + greeting: `Hello ${input.text}`, + }; + }); + +const create = protectedProcedure + .input(z.object({ name: z.string().min(1) })) + .handler(async ({ context, input }) => { + return await context.db.post.create({ + data: { + name: input.name, + createdBy: { connect: { id: context.session.user.id } }, + }, + }); + }); + +const getLatest = protectedProcedure.handler(async ({ context }) => { + const post = await context.db.post.findFirst({ + orderBy: { createdAt: "desc" }, + where: { createdBy: { id: context.session.user.id } }, + }); + return post ?? null; +}); + +const getSecretMessage = protectedProcedure.handler(() => { + return "you can now see this secret message!"; +}); + +export const postRouter = { + hello, + create, + getLatest, + getSecretMessage, +}; diff --git a/cli/template/extras/src/server/api/routers/orpc-post/with-auth.ts b/cli/template/extras/src/server/api/routers/orpc-post/with-auth.ts new file mode 100644 index 0000000000..2172bf9d0e --- /dev/null +++ b/cli/template/extras/src/server/api/routers/orpc-post/with-auth.ts @@ -0,0 +1,49 @@ +import { z } from "zod"; + +import { protectedProcedure, publicProcedure } from "~/server/api/procedures"; + +// Mocked DB +interface Post { + id: number; + name: string; +} +const posts: Post[] = [ + { + id: 1, + name: "Hello World", + }, +]; + +const hello = publicProcedure + .input(z.object({ text: z.string() })) + .handler(({ input }) => { + return { + greeting: `Hello ${input.text}`, + }; + }); + +const create = protectedProcedure + .input(z.object({ name: z.string().min(1) })) + .handler(async ({ input }) => { + const post: Post = { + id: posts.length + 1, + name: input.name, + }; + posts.push(post); + return post; + }); + +const getLatest = protectedProcedure.handler(() => { + return posts.at(-1) ?? null; +}); + +const getSecretMessage = protectedProcedure.handler(() => { + return "you can now see this secret message!"; +}); + +export const postRouter = { + hello, + create, + getLatest, + getSecretMessage, +}; diff --git a/cli/template/extras/src/server/api/routers/orpc-post/with-drizzle.ts b/cli/template/extras/src/server/api/routers/orpc-post/with-drizzle.ts new file mode 100644 index 0000000000..8412d856e0 --- /dev/null +++ b/cli/template/extras/src/server/api/routers/orpc-post/with-drizzle.ts @@ -0,0 +1,33 @@ +import { z } from "zod"; + +import { publicProcedure } from "~/server/api/procedures"; +import { posts } from "~/server/db/schema"; + +const hello = publicProcedure + .input(z.object({ text: z.string() })) + .handler(({ input }) => { + return { + greeting: `Hello ${input.text}`, + }; + }); + +const create = publicProcedure + .input(z.object({ name: z.string().min(1) })) + .handler(async ({ context, input }) => { + return await context.db.insert(posts).values({ + name: input.name, + }); + }); + +const getLatest = publicProcedure.handler(async ({ context }) => { + const post = await context.db.query.posts.findFirst({ + orderBy: (posts, { desc }) => [desc(posts.createdAt)], + }); + return post ?? null; +}); + +export const postRouter = { + hello, + create, + getLatest, +}; diff --git a/cli/template/extras/src/server/api/routers/orpc-post/with-prisma.ts b/cli/template/extras/src/server/api/routers/orpc-post/with-prisma.ts new file mode 100644 index 0000000000..7a1f80b850 --- /dev/null +++ b/cli/template/extras/src/server/api/routers/orpc-post/with-prisma.ts @@ -0,0 +1,34 @@ +import { z } from "zod"; + +import { publicProcedure } from "~/server/api/procedures"; + +const hello = publicProcedure + .input(z.object({ text: z.string() })) + .handler(({ input }) => { + return { + greeting: `Hello ${input.text}`, + }; + }); + +const create = publicProcedure + .input(z.object({ name: z.string().min(1) })) + .handler(async ({ context, input }) => { + return await context.db.post.create({ + data: { + name: input.name, + }, + }); + }); + +const getLatest = publicProcedure.handler(async ({ context }) => { + const post = await context.db.post.findFirst({ + orderBy: { createdAt: "desc" }, + }); + return post ?? null; +}); + +export const postRouter = { + hello, + create, + getLatest, +}; diff --git a/cli/template/extras/src/server/api/routers/post/base.ts b/cli/template/extras/src/server/api/routers/trpc-post/base.ts similarity index 100% rename from cli/template/extras/src/server/api/routers/post/base.ts rename to cli/template/extras/src/server/api/routers/trpc-post/base.ts diff --git a/cli/template/extras/src/server/api/routers/post/with-auth-drizzle.ts b/cli/template/extras/src/server/api/routers/trpc-post/with-auth-drizzle.ts similarity index 100% rename from cli/template/extras/src/server/api/routers/post/with-auth-drizzle.ts rename to cli/template/extras/src/server/api/routers/trpc-post/with-auth-drizzle.ts diff --git a/cli/template/extras/src/server/api/routers/post/with-auth-prisma.ts b/cli/template/extras/src/server/api/routers/trpc-post/with-auth-prisma.ts similarity index 100% rename from cli/template/extras/src/server/api/routers/post/with-auth-prisma.ts rename to cli/template/extras/src/server/api/routers/trpc-post/with-auth-prisma.ts diff --git a/cli/template/extras/src/server/api/routers/post/with-auth.ts b/cli/template/extras/src/server/api/routers/trpc-post/with-auth.ts similarity index 100% rename from cli/template/extras/src/server/api/routers/post/with-auth.ts rename to cli/template/extras/src/server/api/routers/trpc-post/with-auth.ts diff --git a/cli/template/extras/src/server/api/routers/post/with-drizzle.ts b/cli/template/extras/src/server/api/routers/trpc-post/with-drizzle.ts similarity index 100% rename from cli/template/extras/src/server/api/routers/post/with-drizzle.ts rename to cli/template/extras/src/server/api/routers/trpc-post/with-drizzle.ts diff --git a/cli/template/extras/src/server/api/routers/post/with-prisma.ts b/cli/template/extras/src/server/api/routers/trpc-post/with-prisma.ts similarity index 100% rename from cli/template/extras/src/server/api/routers/post/with-prisma.ts rename to cli/template/extras/src/server/api/routers/trpc-post/with-prisma.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 55cb80f08c..8c6f5a837c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -102,6 +102,15 @@ importers: '@libsql/client': specifier: ^0.14.0 version: 0.14.0 + '@orpc/client': + specifier: ^0.53.0 + version: 0.53.0 + '@orpc/react-query': + specifier: ^0.53.0 + version: 0.53.0(@orpc/client@0.53.0)(@tanstack/react-query@5.72.1(react@19.0.0))(react@19.0.0) + '@orpc/server': + specifier: ^0.53.0 + version: 0.53.0(hono@4.7.6)(next@15.2.3(@babel/core@7.24.6)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)) '@planetscale/database': specifier: ^1.19.0 version: 1.19.0 @@ -118,17 +127,20 @@ importers: specifier: ^4.0.15 version: 4.0.15 '@tanstack/react-query': - specifier: ^5.69.0 - version: 5.69.0(react@19.0.0) + specifier: ^5.72.1 + version: 5.72.1(react@19.0.0) + '@tanstack/react-query-next-experimental': + specifier: ^5.72.1 + version: 5.72.1(@tanstack/react-query@5.72.1(react@19.0.0))(next@15.2.3(@babel/core@7.24.6)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0) '@trpc/client': specifier: 11.0.0 version: 11.0.0(@trpc/server@11.0.0(typescript@5.8.2))(typescript@5.8.2) '@trpc/next': specifier: 11.0.0 - version: 11.0.0(@tanstack/react-query@5.69.0(react@19.0.0))(@trpc/client@11.0.0(@trpc/server@11.0.0(typescript@5.8.2))(typescript@5.8.2))(@trpc/react-query@11.0.0(@tanstack/react-query@5.69.0(react@19.0.0))(@trpc/client@11.0.0(@trpc/server@11.0.0(typescript@5.8.2))(typescript@5.8.2))(@trpc/server@11.0.0(typescript@5.8.2))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.8.2))(@trpc/server@11.0.0(typescript@5.8.2))(next@15.2.3(@babel/core@7.24.6)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.8.2) + version: 11.0.0(@tanstack/react-query@5.72.1(react@19.0.0))(@trpc/client@11.0.0(@trpc/server@11.0.0(typescript@5.8.2))(typescript@5.8.2))(@trpc/react-query@11.0.0(@tanstack/react-query@5.72.1(react@19.0.0))(@trpc/client@11.0.0(@trpc/server@11.0.0(typescript@5.8.2))(typescript@5.8.2))(@trpc/server@11.0.0(typescript@5.8.2))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.8.2))(@trpc/server@11.0.0(typescript@5.8.2))(next@15.2.3(@babel/core@7.24.6)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.8.2) '@trpc/react-query': specifier: 11.0.0 - version: 11.0.0(@tanstack/react-query@5.69.0(react@19.0.0))(@trpc/client@11.0.0(@trpc/server@11.0.0(typescript@5.8.2))(typescript@5.8.2))(@trpc/server@11.0.0(typescript@5.8.2))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.8.2) + version: 11.0.0(@tanstack/react-query@5.72.1(react@19.0.0))(@trpc/client@11.0.0(@trpc/server@11.0.0(typescript@5.8.2))(typescript@5.8.2))(@trpc/server@11.0.0(typescript@5.8.2))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.8.2) '@trpc/server': specifier: 11.0.0 version: 11.0.0(typescript@5.8.2) @@ -1644,6 +1656,37 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@orpc/client@0.53.0': + resolution: {integrity: sha512-dxvmvE5htz1TE/K+sUZmmtYqgaPwkxX49Gig0g8M+PLIroWTPCCAkjDK2526wSr1mxkB0IUNo6m8aoJ9JrVH4Q==} + + '@orpc/contract@0.53.0': + resolution: {integrity: sha512-cM771pTET2oSKtFA0qLUibWUSRrTdJC83Zhu24yD/bAP0QOsD6zmGJtvMH7e5cepqVNWTZ1NeQJHkGxVmwqw6w==} + + '@orpc/react-query@0.53.0': + resolution: {integrity: sha512-nl/pxp8pWav816v951YtHI/v6BO4QKlYx4U/btSPVpzZmvq9443ozlnG7sbwPTZxxxd1Z8SoXzzLgkrFJ3GfbA==} + peerDependencies: + '@orpc/client': 0.53.0 + '@tanstack/react-query': '>=5.59.0' + react: '>=18.3.0' + + '@orpc/server@0.53.0': + resolution: {integrity: sha512-mylvzLWr1kADeVtvlE68zEsTktXarRoHZkNydTpMAv4VDWvg+MMjbk2HSV/qUjkc7qT7YhCD0CQyCVEGFRfzwg==} + peerDependencies: + hono: '>=4.6.0' + next: '>=14.0.0' + + '@orpc/shared@0.53.0': + resolution: {integrity: sha512-okEjlWtZ9bs2R/0jN3I+IudANgUoeWmaVIWJtkp/KzEXPhD46NwPbPj0rotMt6kFiZYwPlJ81C41PLEMBfB49A==} + + '@orpc/standard-server-fetch@0.53.0': + resolution: {integrity: sha512-iWQdRWWxNlYXXdG/GMgWL26csiev/zYiq7liq8j1ywOzlKZRPoV0uGv08ptB3PhTKqYrk69AgxgOk9bc4xE21g==} + + '@orpc/standard-server-node@0.53.0': + resolution: {integrity: sha512-B1Yxsq+tZ1gL/rBO8wv49jscvB54bVDZJ5C/UVXVYKLS3w94JaGxUa4hM5V7H98FQc0tHaWJY4hqwMtzdvXFEw==} + + '@orpc/standard-server@0.53.0': + resolution: {integrity: sha512-NdxjMrgTOJmKj92g4yiSvXf4WyqqiYB/EM8HGt46i9el0j2v+3MSNPYFPdTTK1XbBiPhNk81N4RQ5x0shh1BJQ==} + '@oslojs/encoding@1.1.0': resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==} @@ -1952,6 +1995,9 @@ packages: '@stackblitz/sdk@1.11.0': resolution: {integrity: sha512-DFQGANNkEZRzFk1/rDP6TcFdM82ycHE+zfl9C/M/jXlH68jiqHWHFMQURLELoD8koxvu/eW5uhg94NSAZlYrUQ==} + '@standard-schema/spec@1.0.0': + resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} + '@swc/counter@0.1.3': resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} @@ -2066,11 +2112,18 @@ packages: '@tailwindcss/postcss@4.0.15': resolution: {integrity: sha512-qyrpoDKIO7wzkRbKCvGLo7gXRjT9/Njf7ZJiJhG4njrfZkvOhjwnaHpYbpxYeDysEg+9pB1R4jcd+vQ7ZUDsmQ==} - '@tanstack/query-core@5.69.0': - resolution: {integrity: sha512-Kn410jq6vs1P8Nm+ZsRj9H+U3C0kjuEkYLxbiCyn3MDEiYor1j2DGVULqAz62SLZtUZ/e9Xt6xMXiJ3NJ65WyQ==} + '@tanstack/query-core@5.72.1': + resolution: {integrity: sha512-nOu0EEkZuJ0BZnYgeaEfo44+psq1jBO7/zp3KudixD4dvgOVerrhAhDEKsWx2N7MxB59mjO4r0ddP/VqWGPK+Q==} - '@tanstack/react-query@5.69.0': - resolution: {integrity: sha512-Ift3IUNQqTcaFa1AiIQ7WCb/PPy8aexZdq9pZWLXhfLcLxH0+PZqJ2xFImxCpdDZrFRZhLJrh76geevS5xjRhA==} + '@tanstack/react-query-next-experimental@5.72.1': + resolution: {integrity: sha512-tmhQm4Tqhcg8S4DtlXROEN6BLawWzGBAJrbUM/80sXgEX6Ak8D3PvkUFaewzFrIjNtLcu3zTfll34xdr68gilg==} + peerDependencies: + '@tanstack/react-query': ^5.72.1 + next: ^13 || ^14 || ^15 + react: ^18 || ^19 + + '@tanstack/react-query@5.72.1': + resolution: {integrity: sha512-4UEMyRx54xj144D2nDvDIMiXSG5BrqyCJrmyNoGbymNS+VWODcBDFrmRk9p2fe12UGZ4JtKPTNuW2Jg0aisUgQ==} peerDependencies: react: ^18 || ^19 @@ -3830,6 +3883,10 @@ packages: resolution: {integrity: sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==} engines: {node: '>=6'} + hono@4.7.6: + resolution: {integrity: sha512-564rVzELU+9BRqqx5k8sT2NFwGD3I3Vifdb6P7CmM6FiarOSY+fDC+6B+k9wcCb86ReoayteZP2ki0cRLN1jbw==} + engines: {node: '>=16.9.0'} + hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} @@ -5271,6 +5328,10 @@ packages: resolution: {integrity: sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==} engines: {node: '>=8'} + radash@12.1.0: + resolution: {integrity: sha512-b0Zcf09AhqKS83btmUeYBS8tFK7XL2e3RvLmZcm0sTdF1/UUlHSsjXdCcWNxe7yfmAlPve5ym0DmKGtTzP6kVQ==} + engines: {node: '>=14.18.0'} + radix3@1.1.2: resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} @@ -8109,6 +8170,55 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.15.0 + '@orpc/client@0.53.0': + dependencies: + '@orpc/shared': 0.53.0 + '@orpc/standard-server': 0.53.0 + '@orpc/standard-server-fetch': 0.53.0 + + '@orpc/contract@0.53.0': + dependencies: + '@orpc/client': 0.53.0 + '@orpc/shared': 0.53.0 + '@standard-schema/spec': 1.0.0 + + '@orpc/react-query@0.53.0(@orpc/client@0.53.0)(@tanstack/react-query@5.72.1(react@19.0.0))(react@19.0.0)': + dependencies: + '@orpc/client': 0.53.0 + '@orpc/shared': 0.53.0 + '@tanstack/react-query': 5.72.1(react@19.0.0) + react: 19.0.0 + + '@orpc/server@0.53.0(hono@4.7.6)(next@15.2.3(@babel/core@7.24.6)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))': + dependencies: + '@orpc/client': 0.53.0 + '@orpc/contract': 0.53.0 + '@orpc/shared': 0.53.0 + '@orpc/standard-server': 0.53.0 + '@orpc/standard-server-fetch': 0.53.0 + '@orpc/standard-server-node': 0.53.0 + hono: 4.7.6 + next: 15.2.3(@babel/core@7.24.6)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + + '@orpc/shared@0.53.0': + dependencies: + radash: 12.1.0 + type-fest: 4.37.0 + + '@orpc/standard-server-fetch@0.53.0': + dependencies: + '@orpc/shared': 0.53.0 + '@orpc/standard-server': 0.53.0 + + '@orpc/standard-server-node@0.53.0': + dependencies: + '@orpc/shared': 0.53.0 + '@orpc/standard-server': 0.53.0 + + '@orpc/standard-server@0.53.0': + dependencies: + '@orpc/shared': 0.53.0 + '@oslojs/encoding@1.1.0': {} '@panva/hkdf@1.1.1': {} @@ -8371,6 +8481,8 @@ snapshots: '@stackblitz/sdk@1.11.0': {} + '@standard-schema/spec@1.0.0': {} + '@swc/counter@0.1.3': {} '@swc/helpers@0.5.15': @@ -8455,11 +8567,17 @@ snapshots: postcss: 8.5.3 tailwindcss: 4.0.15 - '@tanstack/query-core@5.69.0': {} + '@tanstack/query-core@5.72.1': {} + + '@tanstack/react-query-next-experimental@5.72.1(@tanstack/react-query@5.72.1(react@19.0.0))(next@15.2.3(@babel/core@7.24.6)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)': + dependencies: + '@tanstack/react-query': 5.72.1(react@19.0.0) + next: 15.2.3(@babel/core@7.24.6)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + react: 19.0.0 - '@tanstack/react-query@5.69.0(react@19.0.0)': + '@tanstack/react-query@5.72.1(react@19.0.0)': dependencies: - '@tanstack/query-core': 5.69.0 + '@tanstack/query-core': 5.72.1 react: 19.0.0 '@tanstack/react-virtual@3.13.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': @@ -8477,7 +8595,7 @@ snapshots: '@trpc/server': 11.0.0(typescript@5.8.2) typescript: 5.8.2 - '@trpc/next@11.0.0(@tanstack/react-query@5.69.0(react@19.0.0))(@trpc/client@11.0.0(@trpc/server@11.0.0(typescript@5.8.2))(typescript@5.8.2))(@trpc/react-query@11.0.0(@tanstack/react-query@5.69.0(react@19.0.0))(@trpc/client@11.0.0(@trpc/server@11.0.0(typescript@5.8.2))(typescript@5.8.2))(@trpc/server@11.0.0(typescript@5.8.2))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.8.2))(@trpc/server@11.0.0(typescript@5.8.2))(next@15.2.3(@babel/core@7.24.6)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.8.2)': + '@trpc/next@11.0.0(@tanstack/react-query@5.72.1(react@19.0.0))(@trpc/client@11.0.0(@trpc/server@11.0.0(typescript@5.8.2))(typescript@5.8.2))(@trpc/react-query@11.0.0(@tanstack/react-query@5.72.1(react@19.0.0))(@trpc/client@11.0.0(@trpc/server@11.0.0(typescript@5.8.2))(typescript@5.8.2))(@trpc/server@11.0.0(typescript@5.8.2))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.8.2))(@trpc/server@11.0.0(typescript@5.8.2))(next@15.2.3(@babel/core@7.24.6)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.8.2)': dependencies: '@trpc/client': 11.0.0(@trpc/server@11.0.0(typescript@5.8.2))(typescript@5.8.2) '@trpc/server': 11.0.0(typescript@5.8.2) @@ -8486,12 +8604,12 @@ snapshots: react-dom: 19.0.0(react@19.0.0) typescript: 5.8.2 optionalDependencies: - '@tanstack/react-query': 5.69.0(react@19.0.0) - '@trpc/react-query': 11.0.0(@tanstack/react-query@5.69.0(react@19.0.0))(@trpc/client@11.0.0(@trpc/server@11.0.0(typescript@5.8.2))(typescript@5.8.2))(@trpc/server@11.0.0(typescript@5.8.2))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.8.2) + '@tanstack/react-query': 5.72.1(react@19.0.0) + '@trpc/react-query': 11.0.0(@tanstack/react-query@5.72.1(react@19.0.0))(@trpc/client@11.0.0(@trpc/server@11.0.0(typescript@5.8.2))(typescript@5.8.2))(@trpc/server@11.0.0(typescript@5.8.2))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.8.2) - '@trpc/react-query@11.0.0(@tanstack/react-query@5.69.0(react@19.0.0))(@trpc/client@11.0.0(@trpc/server@11.0.0(typescript@5.8.2))(typescript@5.8.2))(@trpc/server@11.0.0(typescript@5.8.2))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.8.2)': + '@trpc/react-query@11.0.0(@tanstack/react-query@5.72.1(react@19.0.0))(@trpc/client@11.0.0(@trpc/server@11.0.0(typescript@5.8.2))(typescript@5.8.2))(@trpc/server@11.0.0(typescript@5.8.2))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.8.2)': dependencies: - '@tanstack/react-query': 5.69.0(react@19.0.0) + '@tanstack/react-query': 5.72.1(react@19.0.0) '@trpc/client': 11.0.0(@trpc/server@11.0.0(typescript@5.8.2))(typescript@5.8.2) '@trpc/server': 11.0.0(typescript@5.8.2) react: 19.0.0 @@ -10660,6 +10778,8 @@ snapshots: hex-rgb@4.3.0: {} + hono@4.7.6: {} + hosted-git-info@2.8.9: {} html-escaper@3.0.3: {} @@ -12253,6 +12373,8 @@ snapshots: quick-lru@4.0.1: {} + radash@12.1.0: {} + radix3@1.1.2: {} rc@1.2.8: