├── .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 |
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 |

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 |
--------------------------------------------------------------------------------