├── .env.example ├── .eslintrc.js ├── .gitignore ├── .nvmrc ├── .prettierrc.cjs ├── .vscode └── settings.json ├── README.md ├── apps └── nextjs │ ├── .eslintrc.js │ ├── .gitignore │ ├── next-env.d.ts │ ├── next.config.mjs │ ├── package.json │ ├── postcss.config.cjs │ ├── src │ ├── app │ │ ├── client-providers.tsx │ │ ├── header.tsx │ │ ├── layout.tsx │ │ ├── page.tsx │ │ ├── post │ │ │ ├── cached │ │ │ │ ├── layout.tsx │ │ │ │ └── page.tsx │ │ │ ├── create-post.tsx │ │ │ ├── delete-post.tsx │ │ │ ├── hydrated │ │ │ │ ├── layout.tsx │ │ │ │ ├── page.tsx │ │ │ │ └── posts.tsx │ │ │ ├── post.tsx │ │ │ └── rsc │ │ │ │ ├── layout.tsx │ │ │ │ └── page.tsx │ │ ├── protected │ │ │ └── page.tsx │ │ ├── signin │ │ │ └── page.tsx │ │ └── user-panel.tsx │ ├── middleware.ts │ ├── pages │ │ └── api │ │ │ └── trpc │ │ │ └── [trpc].ts │ ├── styles │ │ └── globals.css │ └── trpc │ │ ├── client │ │ └── trpc-client.tsx │ │ ├── rsc │ │ └── trpc.ts │ │ └── types.ts │ ├── tailwind.config.cjs │ └── tsconfig.json ├── package.json ├── packages ├── @trpc │ └── next-layout │ │ ├── client │ │ ├── createHydrateClient.tsx │ │ ├── createTrpcNextBeta.tsx │ │ └── index.ts │ │ ├── package.json │ │ ├── server │ │ ├── createTrpcNextLayout.tsx │ │ ├── index.ts │ │ └── local-storage.ts │ │ └── tsconfig.json ├── api │ ├── index.ts │ ├── package.json │ ├── src │ │ ├── context.ts │ │ ├── router │ │ │ ├── index.ts │ │ │ ├── post.ts │ │ │ └── protected.ts │ │ └── trpc.ts │ ├── transformer.ts │ └── tsconfig.json ├── config │ ├── eslint │ │ ├── index.js │ │ └── package.json │ ├── tailwind │ │ ├── index.js │ │ ├── package.json │ │ └── postcss.js │ └── tsconfig │ │ ├── base.json │ │ ├── nextjs.json │ │ ├── package.json │ │ └── react-library.json └── db │ ├── index.ts │ ├── package.json │ ├── prisma │ └── schema.prisma │ ├── schema.d.ts │ └── tsconfig.json ├── patches └── @tanstack+react-query+4.26.1.patch ├── turbo.json └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | # use the planetscale-js string to connect to your database 2 | # the server must be aws.connect.psdb.cloud 3 | DATABASE_URL="mysql://{username}:{password}@aws.connect.psdb.cloud/{database}?sslaccept=strict" 4 | 5 | # CLERK 6 | 7 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY="public_key" 8 | CLERK_SECRET_KEY="secret_key" -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | // This tells ESLint to load the config from the package `eslint-config-custom` 4 | extends: ["custom"], 5 | settings: { 6 | next: { 7 | rootDir: ["apps/*/"], 8 | }, 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | .env 4 | 5 | # dependencies 6 | node_modules 7 | .pnp 8 | .pnp.js 9 | 10 | # testing 11 | coverage 12 | 13 | # next.js 14 | .next/ 15 | out/ 16 | build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env.local 30 | .env.development.local 31 | .env.test.local 32 | .env.production.local 33 | 34 | # turbo 35 | .turbo 36 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v18 2 | -------------------------------------------------------------------------------- /.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import("prettier").Config} */ 2 | module.exports = { 3 | arrowParens: "always", 4 | printWidth: 80, 5 | singleQuote: false, 6 | jsxSingleQuote: false, 7 | semi: true, 8 | trailingComma: "all", 9 | tabWidth: 2, 10 | }; 11 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "typescript.enablePromptUseWorkspaceTsdk": true 4 | } 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Next.js 13 App Router 2 | 3 | ### Apps and Packages 4 | 5 | - `next`: Next.js 13 with App Router 6 | - `@trpc/next-layout`: Utils to create a tRPC client for Next.js 13. 7 | - `api`: tRPC v10 router used by next on the server 8 | - `db`: Kysely + Prisma 9 | - `config`: `eslint`, `tsconfig` and `tailwind` configs 10 | 11 | ### Features 12 | 13 | - Turborepo 14 | - Edge Runtime for the API + every React Server Component 15 | - tRPC 16 | - [Kysely](https://github.com/koskimas/kysely) for type-safe SQL 17 | - MySQL database hosted to [Planetscale](https://app.planetscale.com) 18 | - Prisma is used to defined the schema + to push it. No @prisma/client is generated, instead we use the [prisma-kysely](https://github.com/valtyr/prisma-kysely) package to generate a `schema.d.ts` for Kysely to infer types. 19 | - [Clerk](https://clerk.dev) for Authentication 20 | 21 | ### tRPC 22 | 23 | You have a few options on how you want to fetch your data with tRPC + Next 13 : 24 | 25 | - Fetch the data in a React Server Component and render the HTML directly ([example](https://github.com/solaldunckel/next-13-app-router-with-trpc/blob/master/apps/nextjs/src/app/post/rsc/page.tsx)) 26 | - Fetch the data in a React Server Component and hydrate the state to a Client Component ([example](https://github.com/solaldunckel/next-13-app-router-with-trpc/blob/master/apps/nextjs/src/app/post/hydrated/page.tsx)) 27 | - Fetch the data in a Client Component 28 | 29 | ### Fetch Cache 30 | 31 | Next.js 13 uses a patched version of `fetch` with additional options to cache your requests. 32 | Since we query the database through HTTP with the [@planetscale/database](https://github.com/planetscale/database-js) package, cache is working at the component level ([example](https://github.com/solaldunckel/next-13-app-router-with-trpc/blob/master/apps/nextjs/src/app/post/cached/page.tsx)). 33 | -------------------------------------------------------------------------------- /apps/nextjs/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ["custom"], 4 | }; 5 | -------------------------------------------------------------------------------- /apps/nextjs/.gitignore: -------------------------------------------------------------------------------- 1 | .vscode -------------------------------------------------------------------------------- /apps/nextjs/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | // NOTE: This file should not be edited 6 | // see https://nextjs.org/docs/basic-features/typescript for more information. 7 | -------------------------------------------------------------------------------- /apps/nextjs/next.config.mjs: -------------------------------------------------------------------------------- 1 | import bundleAnalyzer from "@next/bundle-analyzer"; 2 | 3 | const plugins = []; 4 | 5 | plugins.push( 6 | bundleAnalyzer({ 7 | enabled: process.env.ANALYZE === "true", 8 | }), 9 | ); 10 | 11 | /** @type {import("next").NextConfig} */ 12 | const config = { 13 | reactStrictMode: true, 14 | experimental: { 15 | appDir: true, 16 | typedRoutes: true, 17 | }, 18 | transpilePackages: ["@acme/api", "@acme/db", "@trpc/next-layout"], 19 | }; 20 | 21 | export default plugins.reduce((config, plugin) => plugin(config), config); 22 | -------------------------------------------------------------------------------- /apps/nextjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@acme/nextjs", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "yarn with-env next dev", 7 | "build": "yarn with-env next build", 8 | "start": "yarn with-env next start", 9 | "lint": "next lint", 10 | "with-env": "dotenv -e ../../.env --" 11 | }, 12 | "dependencies": { 13 | "@acme/api": "*", 14 | "@acme/tailwind-config": "*", 15 | "@clerk/nextjs": "^4.15.0", 16 | "@next/bundle-analyzer": "^13.3.0", 17 | "@tanstack/react-query": "4.26.1", 18 | "@tanstack/react-query-devtools": "4.26.1", 19 | "@trpc/client": "^10.16.0", 20 | "@trpc/next-layout": "*", 21 | "@trpc/react-query": "^10.16.0", 22 | "@trpc/server": "^10.16.0", 23 | "next": "^13.3.0", 24 | "react": "^18.2.0", 25 | "react-dom": "^18.2.0", 26 | "server-only": "^0.0.1" 27 | }, 28 | "devDependencies": { 29 | "@acme/tsconfig": "*", 30 | "@babel/core": "^7.21.4", 31 | "@types/node": "^18.15.11", 32 | "@types/react": "^18.0.33", 33 | "@types/react-dom": "^18.0.7", 34 | "dotenv-cli": "^7.2.1", 35 | "eslint": "^8.37.0", 36 | "eslint-config-custom": "*", 37 | "typescript": "^5.0.3" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /apps/nextjs/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | module.exports = require("@acme/tailwind-config/postcss"); 3 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/client-providers.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { trpcClient } from "../trpc/client/trpc-client"; 4 | import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; 5 | import { ClerkProvider } from "@clerk/nextjs/app-beta/client"; 6 | 7 | export function ClientProviders(props: { children: React.ReactNode }) { 8 | return ( 9 | 12 | 13 | {props.children} 14 | 15 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/header.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | export default function Header() { 4 | return ( 5 |
6 | 10 | Home 11 | 12 | 17 | SSR 18 | 19 | 24 | Hydration 25 | 26 | 31 | Cached 32 | 33 |
34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import "../styles/globals.css"; 2 | import { ClientProviders } from "./client-providers"; 3 | import Header from "./header"; 4 | import UserPanel from "./user-panel"; 5 | 6 | export const metadata = { 7 | title: { 8 | template: "%s | Next.js 13 App Router Playground", 9 | default: "Next.js 13 App Router Playground", 10 | }, 11 | description: 12 | "A Next.js 13 App Router Playground on the edge with tRPC, Clerk, Kysely, Planetscale and Prisma", 13 | }; 14 | 15 | export default function RootLayout({ 16 | children, 17 | }: { 18 | children: React.ReactNode; 19 | }) { 20 | return ( 21 | 22 | 23 | 24 |
25 | 26 |
27 | {children} 28 |
29 | 30 | 31 | 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | export default function Page() { 2 | return ( 3 | <> 4 |
5 | Playground for Next.js 13 new App Directory. 6 |
7 |
    8 |
  • • React Server Components
  • 9 |
  • • Edge Runtime
  • 10 |
  • • tRPC
  • 11 |
  • • Kysely + Planetscale + Prisma
  • 12 |
  • • Clerk
  • 13 |
14 | 15 | ); 16 | } 17 | 18 | export const runtime = "edge"; 19 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/post/cached/layout.tsx: -------------------------------------------------------------------------------- 1 | import CreatePost from "../create-post"; 2 | 3 | export default async function Layout({ 4 | children, 5 | }: { 6 | children: React.ReactNode; 7 | }) { 8 | return ( 9 | <> 10 |
11 |

Cached Messages

12 |

revalidate every 30sec

13 |
14 | 15 | {children} 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/post/cached/page.tsx: -------------------------------------------------------------------------------- 1 | import { trpcRsc } from "../../../trpc/rsc/trpc"; 2 | import DeletePost from "../delete-post"; 3 | import Post from "../post"; 4 | 5 | export const metadata = { 6 | title: "RSC Messages", 7 | }; 8 | 9 | export default async function Page() { 10 | const posts = await trpcRsc.post.all.fetch(); 11 | 12 | if (posts.length === 0) { 13 | return

No posts found...

; 14 | } 15 | 16 | return ( 17 | <> 18 |
    19 | {posts.map((post) => ( 20 | 21 | ))} 22 |
23 | 24 | ); 25 | } 26 | 27 | export const revalidate = 30; 28 | export const runtime = "edge"; 29 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/post/create-post.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useRouter } from "next/navigation"; 4 | import { FormEvent, useState, useTransition } from "react"; 5 | import { trpcClient } from "../../trpc/client/trpc-client"; 6 | 7 | export default function CreatePost() { 8 | const router = useRouter(); 9 | const [content, setContent] = useState(""); 10 | const [isPending, startTransition] = useTransition(); 11 | const [mutationDuration, setMutationDuration] = useState(0); 12 | 13 | const createPost = trpcClient.post.create.useMutation({ 14 | onSuccess: () => { 15 | startTransition(() => { 16 | router.refresh(); 17 | }); 18 | }, 19 | }); 20 | 21 | // Create inline loading UI 22 | const isMutating = createPost.isLoading || isPending; 23 | 24 | const onSubmit = async (e: FormEvent) => { 25 | e.preventDefault(); 26 | 27 | const startTime = new Date(); 28 | 29 | await createPost.mutateAsync({ 30 | content, 31 | }); 32 | 33 | const duration = new Date().getTime() - startTime.getTime(); 34 | setMutationDuration(duration); 35 | setContent(""); 36 | }; 37 | 38 | return ( 39 | <> 40 |
41 | setContent(e.target.value)} 47 | disabled={isMutating} 48 | /> 49 | 56 |
57 | {mutationDuration ? ( 58 |
59 |
Duration: {mutationDuration}ms
60 |
61 | ) : null} 62 | 63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/post/delete-post.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useRouter } from "next/navigation"; 4 | import { useTransition } from "react"; 5 | import { trpcClient } from "../../trpc/client/trpc-client"; 6 | 7 | export default function DeletePost({ id }: { id: number }) { 8 | const router = useRouter(); 9 | const [isPending, startTransition] = useTransition(); 10 | 11 | const deletePost = trpcClient.post.delete.useMutation({ 12 | onSuccess: (data) => { 13 | startTransition(() => { 14 | router.refresh(); 15 | }); 16 | }, 17 | }); 18 | 19 | // Create inline loading UI 20 | const isMutating = deletePost.isLoading || isPending; 21 | 22 | return ( 23 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/post/hydrated/layout.tsx: -------------------------------------------------------------------------------- 1 | import CreatePost from "../create-post"; 2 | 3 | export default async function Layout({ 4 | children, 5 | }: { 6 | children: React.ReactNode; 7 | }) { 8 | return ( 9 | <> 10 |
11 |

Hydrated Messages

12 |

1) Fetched on the server

13 |

14 | 2) The state is hydrated by react-query/trpc on the client 15 |

16 |
17 | 18 |
19 | 20 |
21 | 22 | {children} 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/post/hydrated/page.tsx: -------------------------------------------------------------------------------- 1 | import { HydrateClient } from "../../../trpc/client/trpc-client"; 2 | import { trpcRsc } from "../../../trpc/rsc/trpc"; 3 | import { Posts } from "./posts"; 4 | 5 | export const metadata = { 6 | title: "Hydrated Messages", 7 | }; 8 | 9 | export default async function Page() { 10 | const posts = await trpcRsc.post.all.fetch(); 11 | 12 | if (posts.length === 0) { 13 | return

No posts found...

; 14 | } 15 | 16 | const dehydratedState = await trpcRsc.dehydrate(); 17 | 18 | return ( 19 | 20 | 21 | 22 | ); 23 | } 24 | 25 | export const revalidate = 0; 26 | export const runtime = "edge"; 27 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/post/hydrated/posts.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { trpcClient } from "../../../trpc/client/trpc-client"; 4 | import Post from "../post"; 5 | 6 | export function Posts() { 7 | const { data: posts } = trpcClient.post.all.useQuery(); 8 | 9 | if (!posts || posts.length === 0) { 10 | return
No posts
; 11 | } 12 | 13 | return ( 14 |
    15 | {posts.map((post) => ( 16 | 17 | ))} 18 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/post/post.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from "react"; 2 | import type { RouterOutputs } from "../../trpc/types"; 3 | import DeletePost from "./delete-post"; 4 | 5 | type PostProps = { 6 | post: RouterOutputs["post"]["all"][0]; 7 | }; 8 | 9 | const Post: FC = ({ post }) => { 10 | return ( 11 |
12 |
  • {post.content}
  • 13 | 14 |
  • by {post.author ?? "Guest"}
  • 15 |
  • {post.createdAt.toLocaleString()}
  • 16 |
    17 | ); 18 | }; 19 | 20 | export default Post; 21 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/post/rsc/layout.tsx: -------------------------------------------------------------------------------- 1 | import CreatePost from "../create-post"; 2 | 3 | export default async function Layout({ 4 | children, 5 | }: { 6 | children: React.ReactNode; 7 | }) { 8 | return ( 9 | <> 10 |
    11 |

    Server Rendered Messages

    12 |

    1) Fetched on the server

    13 |

    2) Html is sent to the browser

    14 |
    15 | 16 |
    17 | 18 |
    19 | 20 | {children} 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/post/rsc/page.tsx: -------------------------------------------------------------------------------- 1 | import { trpcRsc } from "../../../trpc/rsc/trpc"; 2 | import DeletePost from "../delete-post"; 3 | import Post from "../post"; 4 | 5 | export const metadata = { 6 | title: "RSC Messages", 7 | }; 8 | 9 | export default async function Page() { 10 | const posts = await trpcRsc.post.all.fetch(); 11 | 12 | if (posts.length === 0) { 13 | return

    No posts found...

    ; 14 | } 15 | 16 | return ( 17 |
      18 | {posts.map((post) => ( 19 | 20 | ))} 21 |
    22 | ); 23 | } 24 | 25 | export const revalidate = 0; 26 | export const runtime = "edge"; 27 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/protected/page.tsx: -------------------------------------------------------------------------------- 1 | import { auth, currentUser } from "@clerk/nextjs/app-beta"; 2 | import { redirect } from "next/navigation"; 3 | import { trpcRsc } from "../../trpc/rsc/trpc"; 4 | 5 | export const metadata = { 6 | title: "Protected", 7 | }; 8 | 9 | export default async function Protected() { 10 | const { userId } = auth(); 11 | 12 | if (!userId) { 13 | redirect("/signin"); 14 | } 15 | 16 | const msg = await trpcRsc.protected.message.fetch(); 17 | 18 | return
    {msg}
    ; 19 | } 20 | 21 | export const runtime = "edge"; 22 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/signin/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignIn } from "@clerk/nextjs/app-beta"; 2 | 3 | export const metadata = { 4 | title: "Sign In", 5 | }; 6 | 7 | export default function SignInPage() { 8 | return ; 9 | } 10 | 11 | export const runtime = "edge"; 12 | -------------------------------------------------------------------------------- /apps/nextjs/src/app/user-panel.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useClerk, useUser } from "@clerk/nextjs"; 4 | import Link from "next/link"; 5 | import { useRouter } from "next/navigation"; 6 | 7 | export default function UserPanel() { 8 | const { user, isLoaded } = useUser(); 9 | const { signOut } = useClerk(); 10 | const router = useRouter(); 11 | 12 | const onClick = async () => { 13 | await signOut(); 14 | router.refresh(); 15 | }; 16 | 17 | if (!isLoaded) { 18 | return
    Loading...
    ; 19 | } 20 | 21 | return ( 22 |
    23 | {user ? ( 24 |
    25 |
    26 | {/* eslint-disable-next-line @next/next/no-img-element */} 27 | user_image 32 |
    33 | {user.firstName} {user.lastName} 34 |
    35 |
    36 | 37 | Protected route 38 | 39 | 42 |
    43 | ) : ( 44 | 48 | Sign In 49 | 50 | )} 51 |
    52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /apps/nextjs/src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { withClerkMiddleware } from "@clerk/nextjs/server"; 2 | import { NextResponse } from "next/server"; 3 | 4 | export default withClerkMiddleware(() => { 5 | return NextResponse.next(); 6 | }); 7 | 8 | // Stop Middleware running on static files 9 | export const config = { 10 | matcher: [ 11 | /* 12 | * Match all request paths except for the ones starting with: 13 | * - _next 14 | * - static (static files) 15 | * - favicon.ico (favicon file) 16 | */ 17 | "/(.*?trpc.*?|(?!static|.*\\..*|_next|favicon.ico).*)", 18 | ], 19 | }; 20 | -------------------------------------------------------------------------------- /apps/nextjs/src/pages/api/trpc/[trpc].ts: -------------------------------------------------------------------------------- 1 | import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; 2 | import type { NextRequest } from "next/server"; 3 | import { appRouter } from "@acme/api"; 4 | import { createContextInner } from "@acme/api/src/context"; 5 | import { getAuth } from "@clerk/nextjs/server"; 6 | 7 | export const runtime = "edge"; 8 | 9 | export default function handler(req: NextRequest) { 10 | return fetchRequestHandler({ 11 | endpoint: "/api/trpc", 12 | req, 13 | router: appRouter, 14 | createContext() { 15 | const auth = getAuth(req); 16 | 17 | return createContextInner({ 18 | req, 19 | auth, 20 | }); 21 | }, 22 | onError({ error }) { 23 | if (error.code === "INTERNAL_SERVER_ERROR") { 24 | console.error("Caught TRPC error:", error); 25 | } 26 | }, 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /apps/nextjs/src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /apps/nextjs/src/trpc/client/trpc-client.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import type { AppRouter } from "@acme/api"; 4 | import { httpBatchLink, loggerLink } from "@trpc/react-query"; 5 | import { transformer } from "@acme/api/transformer"; 6 | import { 7 | createHydrateClient, 8 | createTRPCNextBeta, 9 | } from "@trpc/next-layout/client"; 10 | 11 | const getBaseUrl = () => { 12 | if (typeof window !== "undefined") return ""; // browser should use relative url 13 | 14 | if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`; // SSR should use vercel url 15 | 16 | return `http://localhost:${process.env.PORT ?? 3000}`; // dev SSR should use localhost 17 | }; 18 | 19 | /* 20 | * Create a client that can be used in the client only 21 | */ 22 | 23 | export const trpcClient = createTRPCNextBeta({ 24 | queryClientConfig: { 25 | defaultOptions: { 26 | queries: { 27 | refetchOnWindowFocus: false, 28 | cacheTime: Infinity, 29 | staleTime: Infinity, 30 | }, 31 | }, 32 | }, 33 | links: [ 34 | loggerLink({ 35 | enabled: () => true, 36 | }), 37 | httpBatchLink({ 38 | url: `${getBaseUrl()}/api/trpc`, 39 | }), 40 | ], 41 | transformer, 42 | }); 43 | 44 | /* 45 | * A component used to hydrate the state from server to client 46 | */ 47 | export const HydrateClient = createHydrateClient({ 48 | transformer, 49 | }); 50 | -------------------------------------------------------------------------------- /apps/nextjs/src/trpc/rsc/trpc.ts: -------------------------------------------------------------------------------- 1 | import { appRouter } from "@acme/api"; 2 | import { createContextInner } from "@acme/api/src/context"; 3 | import { auth as getAuth } from "@clerk/nextjs/app-beta"; 4 | import superjson from "superjson"; 5 | import { createTRPCNextLayout } from "@trpc/next-layout/server"; 6 | import "server-only"; 7 | 8 | export const trpcRsc = createTRPCNextLayout({ 9 | router: appRouter, 10 | transformer: superjson, 11 | createContext() { 12 | const auth = getAuth(); 13 | 14 | return createContextInner({ 15 | auth, 16 | req: null, 17 | }); 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /apps/nextjs/src/trpc/types.ts: -------------------------------------------------------------------------------- 1 | import type { AppRouter } from "@acme/api"; 2 | import type { inferRouterOutputs } from "@trpc/server"; 3 | 4 | export type RouterOutputs = inferRouterOutputs; 5 | -------------------------------------------------------------------------------- /apps/nextjs/tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import("tailwindcss").Config} */ 2 | module.exports = { 3 | presets: [require("@acme/tailwind-config")], 4 | }; 5 | -------------------------------------------------------------------------------- /apps/nextjs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@acme/tsconfig/nextjs.json", 3 | "compilerOptions": { 4 | "plugins": [ 5 | { 6 | "name": "next" 7 | } 8 | ], 9 | "strictNullChecks": true 10 | }, 11 | "include": [ 12 | "next-env.d.ts", 13 | "**/*.ts", 14 | "**/*.tsx", 15 | "**/*.cjs", 16 | "**/*.mjs", 17 | ".next/types/**/*.ts" 18 | ], 19 | "exclude": [ 20 | "node_modules" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-app-router", 3 | "version": "0.0.0", 4 | "private": true, 5 | "workspaces": [ 6 | "apps/*", 7 | "packages/*", 8 | "packages/config/*", 9 | "packages/@trpc/next-layout" 10 | ], 11 | "scripts": { 12 | "postinstall": "manypkg check && patch-package", 13 | "build": "turbo build", 14 | "dev": "turbo dev --parallel", 15 | "start": "turbo start", 16 | "db:generate": "turbo db:generate", 17 | "db:push": "turbo db:push db:generate", 18 | "lint": "turbo lint", 19 | "format": "prettier --write \"**/*.{ts,tsx,md}\"" 20 | }, 21 | "engines": { 22 | "node": ">=18.0.0" 23 | }, 24 | "dependencies": { 25 | "@manypkg/cli": "^0.20.0", 26 | "eslint-config-custom": "*", 27 | "patch-package": "^6.5.1", 28 | "prettier": "2.8.7", 29 | "turbo": "1.8.8" 30 | }, 31 | "packageManager": "yarn@1.22.19" 32 | } 33 | -------------------------------------------------------------------------------- /packages/@trpc/next-layout/client/createHydrateClient.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { DehydratedState, Hydrate } from "@tanstack/react-query"; 4 | import { DataTransformer } from "@trpc/server"; 5 | import { useMemo } from "react"; 6 | 7 | export function createHydrateClient(opts: { transformer?: DataTransformer }) { 8 | return function HydrateClient(props: { 9 | children: React.ReactNode; 10 | state: DehydratedState; 11 | }) { 12 | const { state, children } = props; 13 | 14 | const transformedState: DehydratedState = useMemo(() => { 15 | if (opts.transformer) { 16 | return opts.transformer.deserialize(state); 17 | } 18 | return state; 19 | }, [state]); 20 | return {children}; 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /packages/@trpc/next-layout/client/createTrpcNextBeta.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import type { CreateTRPCClientOptions } from "@trpc/client"; 4 | import type { AnyRouter, ProtectedIntersection } from "@trpc/server"; 5 | import type { 6 | CreateTRPCReactOptions, 7 | CreateReactUtilsProxy, 8 | CreateTRPCReactQueryClientConfig, 9 | DecoratedProcedureRecord, 10 | } from "@trpc/react-query/shared"; 11 | import { createReactQueryUtilsProxy } from "@trpc/react-query/shared"; 12 | import { useMemo, useState } from "react"; 13 | import { QueryClientProvider } from "@tanstack/react-query"; 14 | import { 15 | createHooksInternal, 16 | getQueryClient, 17 | createReactProxyDecoration, 18 | } from "@trpc/react-query/shared"; 19 | import { createFlatProxy } from "@trpc/server/shared"; 20 | 21 | export type WithTRPCConfig = 22 | CreateTRPCClientOptions & CreateTRPCReactQueryClientConfig; 23 | 24 | type WithTRPCOptions = 25 | CreateTRPCReactOptions & WithTRPCConfig; 26 | 27 | /** 28 | * @internal 29 | */ 30 | export interface CreateTRPCNextBase { 31 | useContext(): CreateReactUtilsProxy; 32 | Provider: ({ children }: { children: React.ReactNode }) => JSX.Element; 33 | } 34 | 35 | /** 36 | * @internal 37 | */ 38 | export type CreateTRPCNext< 39 | TRouter extends AnyRouter, 40 | TFlags, 41 | > = ProtectedIntersection< 42 | CreateTRPCNextBase, 43 | DecoratedProcedureRecord 44 | >; 45 | 46 | export function createTRPCNextBeta( 47 | opts: WithTRPCOptions, 48 | ): CreateTRPCNext { 49 | const trpc = createHooksInternal({ 50 | unstable_overrides: opts.unstable_overrides, 51 | }); 52 | 53 | const TRPCProvider = ({ children }: { children: React.ReactNode }) => { 54 | const [prepassProps] = useState(() => { 55 | const queryClient = getQueryClient(opts); 56 | const trpcClient = trpc.createClient(opts); 57 | return { 58 | queryClient, 59 | trpcClient, 60 | }; 61 | }); 62 | 63 | const { queryClient, trpcClient } = prepassProps; 64 | 65 | return ( 66 | 67 | 68 | {children} 69 | 70 | 71 | ); 72 | }; 73 | 74 | return createFlatProxy((key) => { 75 | if (key === "useContext") { 76 | return () => { 77 | const context = trpc.useContext(); 78 | // create a stable reference of the utils context 79 | return useMemo(() => { 80 | return (createReactQueryUtilsProxy as any)(context); 81 | }, [context]); 82 | }; 83 | } 84 | 85 | if (key === "Provider") { 86 | return TRPCProvider; 87 | } 88 | 89 | return createReactProxyDecoration(key, trpc); 90 | }); 91 | } 92 | -------------------------------------------------------------------------------- /packages/@trpc/next-layout/client/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./createTrpcNextBeta"; 2 | export * from "./createHydrateClient"; 3 | -------------------------------------------------------------------------------- /packages/@trpc/next-layout/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@trpc/next-layout", 3 | "version": "0.1.0", 4 | "main": "./index.ts", 5 | "types": "./index.ts", 6 | "license": "MIT", 7 | "dependencies": { 8 | "@tanstack/react-query": "4.26.1", 9 | "@trpc/client": "^10.16.0", 10 | "@trpc/react-query": "^10.16.0", 11 | "@trpc/server": "^10.16.0", 12 | "next": "^13.3.0", 13 | "react": "^18.2.0", 14 | "server-only": "^0.0.1" 15 | }, 16 | "devDependencies": { 17 | "@acme/tsconfig": "*", 18 | "typescript": "^5.0.3" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/@trpc/next-layout/server/createTrpcNextLayout.tsx: -------------------------------------------------------------------------------- 1 | import type { DehydratedState } from "@tanstack/react-query"; 2 | import type { 3 | AnyProcedure, 4 | AnyQueryProcedure, 5 | AnyRouter, 6 | DataTransformer, 7 | inferProcedureInput, 8 | inferProcedureOutput, 9 | inferRouterContext, 10 | MaybePromise, 11 | ProcedureRouterRecord, 12 | ProcedureType, 13 | } from "@trpc/server"; 14 | import { createRecursiveProxy } from "@trpc/server/shared"; 15 | import { getRequestStorage } from "./local-storage"; 16 | import { dehydrate, QueryClient } from "@tanstack/query-core"; 17 | import "server-only"; 18 | 19 | interface CreateTRPCNextLayoutOptions { 20 | router: TRouter; 21 | createContext: () => MaybePromise>; 22 | transformer?: DataTransformer; 23 | } 24 | 25 | /** 26 | * @internal 27 | */ 28 | export type DecorateProcedure = 29 | TProcedure extends AnyQueryProcedure 30 | ? { 31 | fetch( 32 | input: inferProcedureInput, 33 | ): Promise>; 34 | fetchInfinite( 35 | input: inferProcedureInput, 36 | ): Promise>; 37 | } 38 | : never; 39 | 40 | type OmitNever = Pick< 41 | TType, 42 | { 43 | [K in keyof TType]: TType[K] extends never ? never : K; 44 | }[keyof TType] 45 | >; 46 | /** 47 | * @internal 48 | */ 49 | export type DecoratedProcedureRecord< 50 | TProcedures extends ProcedureRouterRecord, 51 | TPath extends string = "", 52 | > = OmitNever<{ 53 | [TKey in keyof TProcedures]: TProcedures[TKey] extends AnyRouter 54 | ? DecoratedProcedureRecord< 55 | TProcedures[TKey]["_def"]["record"], 56 | `${TPath}${TKey & string}.` 57 | > 58 | : TProcedures[TKey] extends AnyQueryProcedure 59 | ? DecorateProcedure 60 | : never; 61 | }>; 62 | 63 | type CreateTRPCNextLayout = DecoratedProcedureRecord< 64 | TRouter["_def"]["record"] 65 | > & { 66 | dehydrate(): Promise; 67 | }; 68 | 69 | function getQueryKey( 70 | path: string[], 71 | input: unknown, 72 | isFetchInfinite?: boolean, 73 | ) { 74 | return input === undefined 75 | ? [path, { type: isFetchInfinite ? "infinite" : "query" }] // We added { type: "infinite" | "query" }, because it is how trpc v10.0 format the new queryKeys 76 | : [ 77 | path, 78 | { 79 | input: { ...input }, 80 | type: isFetchInfinite ? "infinite" : "query", 81 | }, 82 | ]; 83 | } 84 | 85 | export function createTRPCNextLayout( 86 | opts: CreateTRPCNextLayoutOptions, 87 | ): CreateTRPCNextLayout { 88 | function getState() { 89 | const requestStorage = getRequestStorage<{ 90 | _trpc: { 91 | queryClient: QueryClient; 92 | context: inferRouterContext; 93 | }; 94 | }>(); 95 | requestStorage._trpc = requestStorage._trpc ?? { 96 | cache: Object.create(null), 97 | context: opts.createContext(), 98 | queryClient: new QueryClient({ 99 | defaultOptions: { 100 | queries: { 101 | refetchOnWindowFocus: false, 102 | }, 103 | }, 104 | }), 105 | }; 106 | return requestStorage._trpc; 107 | } 108 | const transformer = opts.transformer ?? { 109 | serialize: (v) => v, 110 | deserialize: (v) => v, 111 | }; 112 | 113 | return createRecursiveProxy(async (callOpts) => { 114 | const path = [...callOpts.path]; 115 | const lastPart = path.pop(); 116 | const state = getState(); 117 | const ctx = await state.context; 118 | const { queryClient } = state; 119 | 120 | if (lastPart === "dehydrate" && path.length === 0) { 121 | if (queryClient.isFetching()) { 122 | await new Promise((resolve) => { 123 | const unsub = queryClient.getQueryCache().subscribe((event) => { 124 | if (event?.query.getObserversCount() === 0) { 125 | resolve(); 126 | unsub(); 127 | } 128 | }); 129 | }); 130 | } 131 | const dehydratedState = dehydrate(queryClient); 132 | 133 | return transformer.serialize(dehydratedState); 134 | } 135 | 136 | const fullPath = path.join("."); 137 | const procedure = opts.router._def.procedures[fullPath] as AnyProcedure; 138 | 139 | const type: ProcedureType = "query"; 140 | 141 | const input = callOpts.args[0]; 142 | const queryKey = getQueryKey(path, input, lastPart === "fetchInfinite"); 143 | 144 | if (lastPart === "fetchInfinite") { 145 | return queryClient.fetchInfiniteQuery(queryKey, () => 146 | procedure({ 147 | rawInput: input, 148 | path: fullPath, 149 | ctx, 150 | type, 151 | }), 152 | ); 153 | } 154 | 155 | return queryClient.fetchQuery(queryKey, () => 156 | procedure({ 157 | rawInput: input, 158 | path: fullPath, 159 | ctx, 160 | type, 161 | }), 162 | ); 163 | }) as CreateTRPCNextLayout; 164 | } 165 | -------------------------------------------------------------------------------- /packages/@trpc/next-layout/server/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./createTrpcNextLayout"; 2 | -------------------------------------------------------------------------------- /packages/@trpc/next-layout/server/local-storage.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file makes sure that we can get a storage that is unique to the current request context 3 | */ 4 | 5 | import type { AsyncLocalStorage } from "async_hooks"; 6 | 7 | // https://github.com/vercel/next.js/blob/canary/packages/next/client/components/request-async-storage.ts 8 | const asyncStorage: AsyncLocalStorage | {} = 9 | require("next/dist/client/components/request-async-storage").requestAsyncStorage; 10 | 11 | function throwError(msg: string) { 12 | throw new Error(msg); 13 | } 14 | 15 | export function getRequestStorage(): T { 16 | if ("getStore" in asyncStorage) { 17 | return asyncStorage.getStore() ?? throwError("Couldn't get async storage"); 18 | } 19 | 20 | return asyncStorage as T; 21 | } 22 | -------------------------------------------------------------------------------- /packages/@trpc/next-layout/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@acme/tsconfig/nextjs.json", 3 | "include": ["server", "client"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/api/index.ts: -------------------------------------------------------------------------------- 1 | export type { AppRouter } from "./src/router"; 2 | export { appRouter } from "./src/router"; 3 | 4 | export { createContext } from "./src/context"; 5 | export type { Context } from "./src/context"; 6 | -------------------------------------------------------------------------------- /packages/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@acme/api", 3 | "version": "0.1.0", 4 | "main": "./index.ts", 5 | "types": "./index.ts", 6 | "license": "MIT", 7 | "scripts": { 8 | "clean": "rm -rf .turbo node_modules", 9 | "lint": "eslint . --ext .ts,.tsx", 10 | "type-check": "tsc --noEmit" 11 | }, 12 | "dependencies": { 13 | "@acme/db": "*", 14 | "@trpc/client": "^10.16.0", 15 | "@trpc/server": "^10.16.0", 16 | "superjson": "^1.12.2", 17 | "zod": "^3.21.4" 18 | }, 19 | "devDependencies": { 20 | "@acme/tsconfig": "*", 21 | "eslint": "^8.37.0", 22 | "typescript": "^5.0.3" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/api/src/context.ts: -------------------------------------------------------------------------------- 1 | import type { inferAsyncReturnType } from "@trpc/server"; 2 | import type { CreateNextContextOptions } from "@trpc/server/adapters/next"; 3 | import type { GetServerSidePropsContext } from "next"; 4 | import { getAuth } from "@clerk/nextjs/server"; 5 | import type { 6 | SignedInAuthObject, 7 | SignedOutAuthObject, 8 | } from "@clerk/nextjs/dist/api"; 9 | import { db } from "@acme/db"; 10 | import type { NextRequest } from "next/server"; 11 | 12 | /** 13 | * Replace this with an object if you want to pass things to createContextInner 14 | */ 15 | type CreateContextOptions = { 16 | auth: SignedInAuthObject | SignedOutAuthObject | null; 17 | req: NextRequest | GetServerSidePropsContext["req"] | null; 18 | }; 19 | 20 | /** Use this helper for: 21 | * - testing, where we dont have to Mock Next.js' req/res 22 | * - trpc's `createSSGHelpers` where we don't have req/res 23 | * @see https://beta.create.t3.gg/en/usage/trpc#-servertrpccontextts 24 | */ 25 | export const createContextInner = async (opts: CreateContextOptions) => { 26 | return { 27 | auth: opts.auth, 28 | req: opts.req, 29 | db, 30 | }; 31 | }; 32 | 33 | /** 34 | * This is the actual context you'll use in your router 35 | * @link https://trpc.io/docs/context 36 | **/ 37 | export const createContext = async (opts: CreateNextContextOptions) => { 38 | const auth = getAuth(opts.req); 39 | 40 | return await createContextInner({ 41 | auth, 42 | req: opts.req, 43 | }); 44 | }; 45 | 46 | export type Context = inferAsyncReturnType; 47 | -------------------------------------------------------------------------------- /packages/api/src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { router } from "../trpc"; 2 | import { postRouter } from "./post"; 3 | import { protectedRouter } from "./protected"; 4 | 5 | export const appRouter = router({ 6 | post: postRouter, 7 | protected: protectedRouter, 8 | }); 9 | 10 | // export type definition of API 11 | export type AppRouter = typeof appRouter; 12 | -------------------------------------------------------------------------------- /packages/api/src/router/post.ts: -------------------------------------------------------------------------------- 1 | import { db } from "@acme/db"; 2 | import { z } from "zod"; 3 | import { publicProcedure, router } from "../trpc"; 4 | import { clerkClient } from "@clerk/nextjs/server"; 5 | 6 | export const postRouter = router({ 7 | all: publicProcedure.query(async ({ ctx }) => { 8 | return db 9 | .selectFrom("Post") 10 | .selectAll() 11 | .orderBy("Post.createdAt", "desc") 12 | .execute(); 13 | }), 14 | 15 | create: publicProcedure 16 | .input( 17 | z.object({ 18 | content: z.string().min(1).max(30), 19 | }), 20 | ) 21 | .mutation(async ({ input, ctx }) => { 22 | let startTime = new Date(); 23 | 24 | const user = ctx.auth?.userId 25 | ? await clerkClient.users.getUser(ctx.auth?.userId) 26 | : null; 27 | 28 | let endTime = new Date(); 29 | 30 | const fetchUserTime = endTime.getTime() - startTime.getTime(); 31 | 32 | startTime = new Date(); 33 | 34 | const res = await db 35 | .insertInto("Post") 36 | .values({ 37 | content: input.content, 38 | author: user?.firstName, 39 | }) 40 | .execute(); 41 | 42 | endTime = new Date(); 43 | 44 | const fetchPostTime = endTime.getTime() - startTime.getTime(); 45 | 46 | console.log(`Fetched user in ${fetchUserTime}ms`); 47 | console.log(`Inserted post in ${fetchPostTime}ms`); 48 | 49 | return res; 50 | }), 51 | 52 | delete: publicProcedure 53 | .input( 54 | z.object({ 55 | id: z.number(), 56 | }), 57 | ) 58 | .mutation(async ({ input }) => { 59 | const startTime = new Date(); 60 | 61 | const res = await db 62 | .deleteFrom("Post") 63 | .where("Post.id", "=", input.id) 64 | .execute(); 65 | 66 | const endTime = new Date(); 67 | 68 | console.log( 69 | `Deleted post in ${endTime.getTime() - startTime.getTime()}ms`, 70 | ); 71 | return res; 72 | }), 73 | }); 74 | -------------------------------------------------------------------------------- /packages/api/src/router/protected.ts: -------------------------------------------------------------------------------- 1 | import { protectedProcedure, router } from "../trpc"; 2 | 3 | export const protectedRouter = router({ 4 | message: protectedProcedure.query(async () => { 5 | return "This message is fetched from a protected procedures."; 6 | }), 7 | }); 8 | -------------------------------------------------------------------------------- /packages/api/src/trpc.ts: -------------------------------------------------------------------------------- 1 | import { initTRPC, TRPCError } from "@trpc/server"; 2 | import { type Context } from "./context"; 3 | import { transformer } from "../transformer"; 4 | 5 | const t = initTRPC.context().create({ 6 | transformer, 7 | errorFormatter({ shape }) { 8 | return shape; 9 | }, 10 | }); 11 | 12 | const isAuthed = t.middleware(async ({ ctx, next }) => { 13 | if (!ctx.auth?.userId) { 14 | throw new TRPCError({ 15 | code: "UNAUTHORIZED", 16 | message: "Not authenticated", 17 | }); 18 | } 19 | 20 | return next({ 21 | ctx: { 22 | ...ctx, 23 | auth: ctx.auth, 24 | }, 25 | }); 26 | }); 27 | 28 | export const router = t.router; 29 | export const publicProcedure = t.procedure; 30 | export const protectedProcedure = t.procedure.use(isAuthed); 31 | -------------------------------------------------------------------------------- /packages/api/transformer.ts: -------------------------------------------------------------------------------- 1 | import superjson from "superjson"; 2 | export const transformer = superjson; 3 | -------------------------------------------------------------------------------- /packages/api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@acme/tsconfig/base.json", 3 | "include": ["src", "index.ts", "transformer.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/config/eslint/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["next", "prettier"], 3 | rules: { 4 | "@next/next/no-html-link-for-pages": "off", 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /packages/config/eslint/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-config-custom", 3 | "version": "0.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "eslint": "^8.37.0", 8 | "eslint-config-next": "13.3.0", 9 | "eslint-config-prettier": "^8.8.0", 10 | "eslint-config-turbo": "1.8.8", 11 | "eslint-plugin-react": "7.32.2" 12 | }, 13 | "devDependencies": { 14 | "typescript": "^5.0.3" 15 | }, 16 | "publishConfig": { 17 | "access": "public" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/config/tailwind/index.js: -------------------------------------------------------------------------------- 1 | const { fontFamily } = require("tailwindcss/defaultTheme"); 2 | 3 | /** @type {import('tailwindcss').Config} */ 4 | module.exports = { 5 | content: ["./src/**/*.{ts,tsx}"], 6 | theme: {}, 7 | }; 8 | -------------------------------------------------------------------------------- /packages/config/tailwind/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@acme/tailwind-config", 3 | "version": "0.1.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "files": [ 7 | "index.js", 8 | "postcss.js" 9 | ], 10 | "devDependencies": { 11 | "autoprefixer": "^10.4.14", 12 | "postcss": "^8.4.21", 13 | "tailwindcss": "^3.3.1" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/config/tailwind/postcss.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /packages/config/tsconfig/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Default", 4 | "compilerOptions": { 5 | "composite": false, 6 | "declaration": true, 7 | "declarationMap": true, 8 | "esModuleInterop": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "inlineSources": false, 11 | "isolatedModules": true, 12 | "moduleResolution": "node", 13 | "noUnusedLocals": false, 14 | "noUnusedParameters": false, 15 | "preserveWatchOutput": true, 16 | "skipLibCheck": true, 17 | "strict": true 18 | }, 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /packages/config/tsconfig/nextjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Next.js", 4 | "extends": "./base.json", 5 | "compilerOptions": { 6 | "target": "es5", 7 | "lib": ["dom", "dom.iterable", "esnext"], 8 | "allowJs": true, 9 | "skipLibCheck": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "noEmit": true, 13 | "incremental": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "jsx": "preserve" 19 | }, 20 | "include": ["src", "next-env.d.ts"], 21 | "exclude": ["node_modules"] 22 | } 23 | -------------------------------------------------------------------------------- /packages/config/tsconfig/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@acme/tsconfig", 3 | "version": "0.0.0", 4 | "private": true, 5 | "files": [ 6 | "base.json", 7 | "nextjs.json", 8 | "react-library.json" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /packages/config/tsconfig/react-library.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "React Library", 4 | "extends": "./base.json", 5 | "compilerOptions": { 6 | "jsx": "react-jsx", 7 | "lib": ["ES2015"], 8 | "module": "ESNext", 9 | "target": "es6" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/db/index.ts: -------------------------------------------------------------------------------- 1 | import { Kysely } from "kysely"; 2 | import type { DB } from "./schema"; 3 | import { PlanetScaleDialect } from "kysely-planetscale"; 4 | 5 | export type KyselyClient = Kysely; 6 | 7 | export const db = new Kysely({ 8 | dialect: new PlanetScaleDialect({ 9 | url: process.env.DATABASE_URL, 10 | fetch: (url, options) => { 11 | return fetch(url, { 12 | cache: "default", 13 | ...options, 14 | }); 15 | }, 16 | }), 17 | }); 18 | 19 | export type { DB } from "./schema"; 20 | export type { 21 | InsertObject, 22 | ExpressionBuilder, 23 | StringReference, 24 | Selectable, 25 | Insertable, 26 | } from "kysely"; 27 | export { sql } from "kysely"; 28 | -------------------------------------------------------------------------------- /packages/db/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@acme/db", 3 | "version": "0.1.0", 4 | "main": "./index.ts", 5 | "types": "./index.ts", 6 | "type": "module", 7 | "license": "MIT", 8 | "scripts": { 9 | "clean": "rm -rf .turbo node_modules", 10 | "with-env": "dotenv -e ../../.env --", 11 | "dev": "yarn db:studio", 12 | "db:studio": "yarn with-env prisma studio --port 5556 --browser none", 13 | "db:generate": "yarn with-env prisma generate", 14 | "db:push": "yarn with-env prisma db push", 15 | "db:reset": "yarn with-env prisma migrate reset --skip-generate", 16 | "type-check": "tsc --noEmit" 17 | }, 18 | "dependencies": { 19 | "@planetscale/database": "^1.7.0", 20 | "kysely": "^0.24.2", 21 | "kysely-planetscale": "^1.3.0" 22 | }, 23 | "devDependencies": { 24 | "dotenv-cli": "^7.2.1", 25 | "prisma": "^4.12.0", 26 | "prisma-kysely": "^1.1.0", 27 | "typescript": "^5.0.3" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/db/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator kysely { 5 | provider = "prisma-kysely" 6 | output = "../" 7 | fileName = "schema.d.ts" 8 | } 9 | 10 | datasource db { 11 | provider = "mysql" 12 | url = env("DATABASE_URL") 13 | relationMode = "prisma" 14 | } 15 | 16 | model Post { 17 | id Int @id @default(autoincrement()) 18 | createdAt DateTime @default(now()) 19 | updatedAt DateTime @default(now()) @updatedAt 20 | author String? 21 | content String? 22 | } 23 | -------------------------------------------------------------------------------- /packages/db/schema.d.ts: -------------------------------------------------------------------------------- 1 | import type { ColumnType } from "kysely"; 2 | export type Generated = T extends ColumnType 3 | ? ColumnType 4 | : ColumnType; 5 | export type Timestamp = ColumnType; 6 | export type Post = { 7 | id: Generated; 8 | createdAt: Generated; 9 | updatedAt: Generated; 10 | author: string | null; 11 | content: string | null; 12 | }; 13 | export type DB = { 14 | Post: Post; 15 | }; 16 | -------------------------------------------------------------------------------- /packages/db/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@acme/tsconfig/base.json", 3 | "compilerOptions": { 4 | "module": "ES2020" 5 | }, 6 | "include": ["index.ts", "schema.d.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /patches/@tanstack+react-query+4.26.1.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/@tanstack/react-query/build/lib/reactBatchedUpdates.mjs b/node_modules/@tanstack/react-query/build/lib/reactBatchedUpdates.mjs 2 | index 8a5ec0f..e9f76e5 100644 3 | --- a/node_modules/@tanstack/react-query/build/lib/reactBatchedUpdates.mjs 4 | +++ b/node_modules/@tanstack/react-query/build/lib/reactBatchedUpdates.mjs 5 | @@ -1,6 +1,6 @@ 6 | -import * as ReactDOM from 'react-dom'; 7 | - 8 | -const unstable_batchedUpdates = ReactDOM.unstable_batchedUpdates; 9 | +const unstable_batchedUpdates = (callback) => { 10 | + callback() 11 | +}; 12 | 13 | export { unstable_batchedUpdates }; 14 | //# sourceMappingURL=reactBatchedUpdates.mjs.map 15 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "globalDependencies": ["DATABASE_URL"], 4 | "pipeline": { 5 | "db:generate": { 6 | "inputs": ["prisma/schema.prisma"], 7 | "outputs": ["schema.d.ts"] 8 | }, 9 | "db:push": { 10 | "inputs": ["prisma/schema.prisma"], 11 | "cache": false 12 | }, 13 | "build": { 14 | "dependsOn": ["^build", "^db:generate"], 15 | "outputs": ["dist/**", ".next/**"] 16 | }, 17 | "start": { 18 | "cache": false 19 | }, 20 | "lint": { 21 | "outputs": [] 22 | }, 23 | "dev": { 24 | "dependsOn": ["^db:generate"], 25 | "cache": false, 26 | "persistent": true 27 | } 28 | } 29 | } 30 | --------------------------------------------------------------------------------