From e71bedde0d6944fa7719a5f97cd27a1503156faa Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 16 Oct 2023 00:13:53 +0200 Subject: [PATCH] feat: appdir option (smaller version) (#1567) Co-authored-by: c-ehrlich --- .changeset/calm-maps-allow.md | 5 + .github/workflows/e2e.yml | 15 +- cli/package.json | 3 +- cli/src/cli/index.ts | 17 +++ cli/src/helpers/createProject.ts | 45 +++++- cli/src/helpers/selectBoilerplate.ts | 68 +++++++-- cli/src/index.ts | 5 +- cli/src/installers/dependencyVersionMap.ts | 1 + cli/src/installers/drizzle.ts | 3 +- cli/src/installers/index.ts | 1 + cli/src/installers/nextAuth.ts | 16 ++- cli/src/installers/prisma.ts | 1 + cli/src/installers/tailwind.ts | 4 - cli/src/installers/trpc.ts | 71 ++++++++-- cli/template/base/_eslintrc.cjs | 6 + cli/template/base/tsconfig.json | 6 +- .../extras/config/next-config-appdir.mjs | 10 ++ cli/template/extras/config/tailwind.config.ts | 9 +- cli/template/extras/prisma/schema/base.prisma | 2 +- .../extras/prisma/schema/with-auth.prisma | 6 +- .../src/app/_components/create-post-tw.tsx | 43 ++++++ .../src/app/_components/create-post.tsx | 44 ++++++ .../src/app/api/auth/[...nextauth]/route.ts | 7 + .../extras/src/app/api/trpc/[trpc]/route.ts | 24 ++++ cli/template/extras/src/app/layout/base.tsx | 25 ++++ .../extras/src/app/layout/with-trpc-tw.tsx | 31 +++++ .../extras/src/app/layout/with-trpc.tsx | 30 ++++ .../extras/src/app/layout/with-tw.tsx | 26 ++++ cli/template/extras/src/app/page/base.tsx | 39 ++++++ .../extras/src/app/page/with-auth-trpc-tw.tsx | 82 +++++++++++ .../extras/src/app/page/with-auth-trpc.tsx | 85 ++++++++++++ .../extras/src/app/page/with-trpc-tw.tsx | 65 +++++++++ .../extras/src/app/page/with-trpc.tsx | 68 +++++++++ cli/template/extras/src/app/page/with-tw.tsx | 37 +++++ .../src/pages => extras/src}/index.module.css | 28 ++++ .../src/pages/_app/base.tsx} | 0 .../src/pages/index/base.tsx} | 0 .../src/pages/index/with-auth-trpc-tw.tsx | 4 +- .../extras/src/pages/index/with-auth-trpc.tsx | 4 +- .../extras/src/pages/index/with-trpc-tw.tsx | 2 +- .../extras/src/pages/index/with-trpc.tsx | 2 +- cli/template/extras/src/server/api/root.ts | 4 +- .../src/server/api/routers/example/base.ts | 13 -- .../api/routers/example/with-auth-drizzle.ts | 25 ---- .../api/routers/example/with-auth-prisma.ts | 25 ---- .../server/api/routers/example/with-auth.ts | 21 --- .../api/routers/example/with-drizzle.ts | 16 --- .../server/api/routers/example/with-prisma.ts | 16 --- .../src/server/api/routers/post/base.ts | 32 +++++ .../api/routers/post/with-auth-drizzle.ts | 40 ++++++ .../api/routers/post/with-auth-prisma.ts | 42 ++++++ .../src/server/api/routers/post/with-auth.ts | 40 ++++++ .../server/api/routers/post/with-drizzle.ts | 31 +++++ .../server/api/routers/post/with-prisma.ts | 32 +++++ .../extras/src/server/api/trpc-app/base.ts | 99 +++++++++++++ .../src/server/api/trpc-app/with-auth-db.ts | 130 ++++++++++++++++++ .../src/server/api/trpc-app/with-auth.ts | 127 +++++++++++++++++ .../extras/src/server/api/trpc-app/with-db.ts | 102 ++++++++++++++ .../server/api/{trpc => trpc-pages}/base.ts | 0 .../api/{trpc => trpc-pages}/with-auth-db.ts | 0 .../api/{trpc => trpc-pages}/with-auth.ts | 0 .../api/{trpc => trpc-pages}/with-db.ts | 0 .../extras/src/server/auth-app/base.ts | 68 +++++++++ .../src/server/auth-app/with-drizzle.ts | 72 ++++++++++ .../extras/src/server/auth-app/with-prisma.ts | 71 ++++++++++ .../src/server/{auth => auth-pages}/base.ts | 0 .../{auth => auth-pages}/with-drizzle.ts | 0 .../{auth => auth-pages}/with-prisma.ts | 0 .../src/server/db/drizzle-schema-auth.ts | 9 +- .../src/server/db/drizzle-schema-base.ts | 8 +- cli/template/extras/src/trpc/react.tsx | 47 +++++++ cli/template/extras/src/trpc/server.ts | 28 ++++ cli/template/extras/src/trpc/shared.ts | 30 ++++ pnpm-lock.yaml | 20 ++- 74 files changed, 1897 insertions(+), 191 deletions(-) create mode 100644 .changeset/calm-maps-allow.md create mode 100644 cli/template/extras/config/next-config-appdir.mjs create mode 100644 cli/template/extras/src/app/_components/create-post-tw.tsx create mode 100644 cli/template/extras/src/app/_components/create-post.tsx create mode 100644 cli/template/extras/src/app/api/auth/[...nextauth]/route.ts create mode 100644 cli/template/extras/src/app/api/trpc/[trpc]/route.ts create mode 100644 cli/template/extras/src/app/layout/base.tsx create mode 100644 cli/template/extras/src/app/layout/with-trpc-tw.tsx create mode 100644 cli/template/extras/src/app/layout/with-trpc.tsx create mode 100644 cli/template/extras/src/app/layout/with-tw.tsx create mode 100644 cli/template/extras/src/app/page/base.tsx create mode 100644 cli/template/extras/src/app/page/with-auth-trpc-tw.tsx create mode 100644 cli/template/extras/src/app/page/with-auth-trpc.tsx create mode 100644 cli/template/extras/src/app/page/with-trpc-tw.tsx create mode 100644 cli/template/extras/src/app/page/with-trpc.tsx create mode 100644 cli/template/extras/src/app/page/with-tw.tsx rename cli/template/{base/src/pages => extras/src}/index.module.css (82%) rename cli/template/{base/src/pages/_app.tsx => extras/src/pages/_app/base.tsx} (100%) rename cli/template/{base/src/pages/index.tsx => extras/src/pages/index/base.tsx} (100%) delete mode 100644 cli/template/extras/src/server/api/routers/example/base.ts delete mode 100644 cli/template/extras/src/server/api/routers/example/with-auth-drizzle.ts delete mode 100644 cli/template/extras/src/server/api/routers/example/with-auth-prisma.ts delete mode 100644 cli/template/extras/src/server/api/routers/example/with-auth.ts delete mode 100644 cli/template/extras/src/server/api/routers/example/with-drizzle.ts delete mode 100644 cli/template/extras/src/server/api/routers/example/with-prisma.ts create mode 100644 cli/template/extras/src/server/api/routers/post/base.ts create mode 100644 cli/template/extras/src/server/api/routers/post/with-auth-drizzle.ts create mode 100644 cli/template/extras/src/server/api/routers/post/with-auth-prisma.ts create mode 100644 cli/template/extras/src/server/api/routers/post/with-auth.ts create mode 100644 cli/template/extras/src/server/api/routers/post/with-drizzle.ts create mode 100644 cli/template/extras/src/server/api/routers/post/with-prisma.ts create mode 100644 cli/template/extras/src/server/api/trpc-app/base.ts create mode 100644 cli/template/extras/src/server/api/trpc-app/with-auth-db.ts create mode 100644 cli/template/extras/src/server/api/trpc-app/with-auth.ts create mode 100644 cli/template/extras/src/server/api/trpc-app/with-db.ts rename cli/template/extras/src/server/api/{trpc => trpc-pages}/base.ts (100%) rename cli/template/extras/src/server/api/{trpc => trpc-pages}/with-auth-db.ts (100%) rename cli/template/extras/src/server/api/{trpc => trpc-pages}/with-auth.ts (100%) rename cli/template/extras/src/server/api/{trpc => trpc-pages}/with-db.ts (100%) create mode 100644 cli/template/extras/src/server/auth-app/base.ts create mode 100644 cli/template/extras/src/server/auth-app/with-drizzle.ts create mode 100644 cli/template/extras/src/server/auth-app/with-prisma.ts rename cli/template/extras/src/server/{auth => auth-pages}/base.ts (100%) rename cli/template/extras/src/server/{auth => auth-pages}/with-drizzle.ts (100%) rename cli/template/extras/src/server/{auth => auth-pages}/with-prisma.ts (100%) create mode 100644 cli/template/extras/src/trpc/react.tsx create mode 100644 cli/template/extras/src/trpc/server.ts create mode 100644 cli/template/extras/src/trpc/shared.ts diff --git a/.changeset/calm-maps-allow.md b/.changeset/calm-maps-allow.md new file mode 100644 index 0000000000..7e3007c719 --- /dev/null +++ b/.changeset/calm-maps-allow.md @@ -0,0 +1,5 @@ +--- +"create-t3-app": minor +--- + +feat: add app router option diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index eea74db5a5..cb12c5c5e3 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -27,9 +27,10 @@ jobs: tailwind: ["true", "false"] nextAuth: ["true", "false"] prisma: ["true", "false"] + appRouter: ["true", "false"] drizzle: ["true", "false"] - name: "Build and Start T3 App ${{ matrix.trpc }}-${{ matrix.tailwind }}-${{ matrix.nextAuth }}-${{ matrix.prisma }}-${{ matrix.drizzle}}" + name: "Build and Start T3 App ${{ matrix.trpc }}-${{ matrix.tailwind }}-${{ matrix.nextAuth }}-${{ matrix.prisma }}-${{ matrix.drizzle}}-${{ matrix.appRouter }}" steps: - uses: actions/checkout@v3 with: @@ -69,19 +70,19 @@ jobs: ${{ runner.os }}-pnpm-store- - name: Install dependencies - if: ${{ steps.matrix-valid.outputs.continue == 'true' }} + if: ${{ steps.matrix-valid.outputs.continue == 'true' }} run: pnpm install - run: pnpm turbo --filter=create-t3-app build - if: ${{ steps.matrix-valid.outputs.continue == 'true' }} + if: ${{ steps.matrix-valid.outputs.continue == 'true' }} # has to be scaffolded outside the CLI project so that no lint/tsconfig are leaking # through. this way it ensures that it is the app's configs that are being used # FIXME: this is a bit hacky, would rather have --packages=trpc,tailwind,... but not sure how to setup the matrix for that - - run: cd cli && pnpm start ../../ci-${{ matrix.trpc }}-${{ matrix.tailwind }}-${{ matrix.nextAuth }}-${{ matrix.prisma }}-${{ matrix.drizzle}} --noGit --CI --trpc=${{ matrix.trpc }} --tailwind=${{ matrix.tailwind }} --nextAuth=${{ matrix.nextAuth }} --prisma=${{ matrix.prisma }} --drizzle=${{ matrix.drizzle }} - if: ${{ steps.matrix-valid.outputs.continue == 'true' }} - - run: cd ../ci-${{ matrix.trpc }}-${{ matrix.tailwind }}-${{ matrix.nextAuth }}-${{ matrix.prisma }}-${{ matrix.drizzle}} && pnpm build - if: ${{ steps.matrix-valid.outputs.continue == 'true' }} + - run: cd cli && pnpm start ../../ci-${{ matrix.trpc }}-${{ matrix.tailwind }}-${{ matrix.nextAuth }}-${{ matrix.prisma }}-${{ matrix.drizzle}}-${{ matrix.appRouter }} --noGit --CI --trpc=${{ matrix.trpc }} --tailwind=${{ matrix.tailwind }} --nextAuth=${{ matrix.nextAuth }} --prisma=${{ matrix.prisma }} --drizzle=${{ matrix.drizzle }} --appRouter=${{ matrix.appRouter }} + if: ${{ steps.matrix-valid.outputs.continue == 'true' }} + - run: cd ../ci-${{ matrix.trpc }}-${{ matrix.tailwind }}-${{ matrix.nextAuth }}-${{ matrix.prisma }}-${{ matrix.drizzle}}-${{ matrix.appRouter }} && pnpm build + if: ${{ steps.matrix-valid.outputs.continue == 'true' }} env: NEXTAUTH_SECRET: foo DATABASE_URL: mysql://root:root@localhost:3306/test # can't use url from example env cause we block that in t3-env diff --git a/cli/package.json b/cli/package.json index a839cd9213..fa4d70ee96 100644 --- a/cli/package.json +++ b/cli/package.json @@ -40,7 +40,7 @@ "dev": "tsup --watch", "clean": "rm -rf dist .turbo node_modules", "start": "node dist/index.js", - "lint": "eslint . --report-unused-disable-directives", + "lint": "eslint . --ext .ts,.tsx", "lint:fix": "pnpm lint --fix", "format": "prettier '**/*.{cjs,mjs,ts,tsx,md,json}' --ignore-path ../.gitignore --ignore-unknown --no-error-on-unmatched-pattern --write", "format:check": "prettier '**/*.{cjs,mjs,ts,tsx,md,json}' --ignore-path ../.gitignore --ignore-unknown --no-error-on-unmatched-pattern --check", @@ -86,6 +86,7 @@ "react": "18.2.0", "react-dom": "18.2.0", "superjson": "^1.12.2", + "tailwindcss": "^3.3.2", "tsup": "^6.7.0", "type-fest": "^3.7.0", "typescript": "^5.0.4", diff --git a/cli/src/cli/index.ts b/cli/src/cli/index.ts index 8f07b9a6d2..22125f30a4 100644 --- a/cli/src/cli/index.ts +++ b/cli/src/cli/index.ts @@ -29,6 +29,8 @@ interface CliFlags { drizzle: boolean; /** @internal Used in CI. */ nextAuth: boolean; + /** @internal Used in CI. */ + appRouter: boolean; } interface CliResults { @@ -51,6 +53,7 @@ const defaultOptions: CliResults = { drizzle: false, nextAuth: false, importAlias: "~/", + appRouter: false, }, }; @@ -121,6 +124,11 @@ export const runCli = async (): Promise => { "Explicitly tell the CLI to use a custom import alias", defaultOptions.flags.importAlias ) + .option( + "--appRouter [boolean]", + "Explicitly tell the CLI to use the new Next.js app router", + (value) => !!value && value !== "false" + ) /** END CI-FLAGS */ .version(getVersion(), "-v, --version", "Display the version number") .addHelpText( @@ -246,6 +254,14 @@ export const runCli = async (): Promise => { initialValue: "none", }); }, + appRouter: () => { + return p.confirm({ + message: + chalk.bgCyan(" EXPERIMENTAL ") + + " Would you like to use Next.js App Router?", + initialValue: false, + }); + }, ...(!cliResults.flags.noGit && { git: () => { return p.confirm({ @@ -293,6 +309,7 @@ export const runCli = async (): Promise => { packages, flags: { ...cliResults.flags, + appRouter: project.appRouter ?? cliResults.flags.appRouter, noGit: !project.git ?? cliResults.flags.noGit, noInstall: !project.install ?? cliResults.flags.noInstall, importAlias: project.importAlias ?? cliResults.flags.importAlias, diff --git a/cli/src/helpers/createProject.ts b/cli/src/helpers/createProject.ts index 74d768c8cd..df50b78015 100644 --- a/cli/src/helpers/createProject.ts +++ b/cli/src/helpers/createProject.ts @@ -1,8 +1,15 @@ +import fs from "fs"; import path from "path"; +import { PKG_ROOT } from "~/consts.js"; import { installPackages } from "~/helpers/installPackages.js"; import { scaffoldProject } from "~/helpers/scaffoldProject.js"; -import { selectAppFile, selectIndexFile } from "~/helpers/selectBoilerplate.js"; +import { + selectAppFile, + selectIndexFile, + selectLayoutFile, + selectPageFile, +} from "~/helpers/selectBoilerplate.js"; import { type PkgInstallerMap } from "~/installers/index.js"; import { getUserPkgManager } from "~/utils/getUserPkgManager.js"; @@ -12,6 +19,7 @@ interface CreateProjectOptions { scopedAppName: string; noInstall: boolean; importAlias: string; + appRouter: boolean; } export const createProject = async ({ @@ -19,6 +27,7 @@ export const createProject = async ({ scopedAppName, packages, noInstall, + appRouter, }: CreateProjectOptions) => { const pkgManager = getUserPkgManager(); const projectDir = path.resolve(process.cwd(), projectName); @@ -30,6 +39,7 @@ export const createProject = async ({ pkgManager, scopedAppName, noInstall, + appRouter, }); // Install the selected packages @@ -40,11 +50,38 @@ export const createProject = async ({ pkgManager, packages, noInstall, + appRouter, }); - // TODO: Look into using handlebars or other templating engine to scaffold without needing to maintain multiple copies of the same file - selectAppFile({ projectDir, packages }); - selectIndexFile({ projectDir, packages }); + // Select necessary _app,index / layout,page files + if (appRouter) { + // Replace next.config + fs.copyFileSync( + path.join(PKG_ROOT, "template/extras/config/next-config-appdir.mjs"), + path.join(projectDir, "next.config.mjs") + ); + + selectLayoutFile({ projectDir, packages }); + selectPageFile({ projectDir, packages }); + } else { + selectAppFile({ projectDir, packages }); + selectIndexFile({ projectDir, packages }); + } + + // If no tailwind, select use css modules + if (!packages.tailwind.inUse) { + const indexModuleCss = path.join( + PKG_ROOT, + "template/extras/src/index.module.css" + ); + const indexModuleCssDest = path.join( + projectDir, + "src", + appRouter ? "app" : "pages", + "index.module.css" + ); + fs.copyFileSync(indexModuleCss, indexModuleCssDest); + } return projectDir; }; diff --git a/cli/src/helpers/selectBoilerplate.ts b/cli/src/helpers/selectBoilerplate.ts index 514be93d0f..adee1b0bf9 100644 --- a/cli/src/helpers/selectBoilerplate.ts +++ b/cli/src/helpers/selectBoilerplate.ts @@ -17,7 +17,7 @@ export const selectAppFile = ({ const usingTRPC = packages.trpc.inUse; const usingNextAuth = packages.nextAuth.inUse; - let appFile = ""; + let appFile = "base.tsx"; if (usingNextAuth && usingTRPC) { appFile = "with-auth-trpc.tsx"; } else if (usingNextAuth && !usingTRPC) { @@ -26,11 +26,32 @@ export const selectAppFile = ({ appFile = "with-trpc.tsx"; } - if (appFile !== "") { - const appSrc = path.join(appFileDir, appFile); - const appDest = path.join(projectDir, "src/pages/_app.tsx"); - fs.copySync(appSrc, appDest); + const appSrc = path.join(appFileDir, appFile); + const appDest = path.join(projectDir, "src/pages/_app.tsx"); + fs.copySync(appSrc, appDest); +}; + +// Similar to _app, but for app router +export const selectLayoutFile = ({ + projectDir, + packages, +}: SelectBoilerplateProps) => { + const layoutFileDir = path.join(PKG_ROOT, "template/extras/src/app/layout"); + + const usingTw = packages.tailwind.inUse; + const usingTRPC = packages.trpc.inUse; + let layoutFile = "base.tsx"; + if (usingTRPC && usingTw) { + layoutFile = "with-trpc-tw.tsx"; + } else if (usingTRPC && !usingTw) { + layoutFile = "with-trpc.tsx"; + } else if (!usingTRPC && usingTw) { + layoutFile = "with-tw.tsx"; } + + const appSrc = path.join(layoutFileDir, layoutFile); + const appDest = path.join(projectDir, "src/app/layout.tsx"); + fs.copySync(appSrc, appDest); }; // This selects the proper index.tsx to be used that showcases the chosen tech @@ -44,7 +65,7 @@ export const selectIndexFile = ({ const usingTw = packages.tailwind.inUse; const usingAuth = packages.nextAuth.inUse; - let indexFile = ""; + let indexFile = "base.tsx"; if (usingTRPC && usingTw && usingAuth) { indexFile = "with-auth-trpc-tw.tsx"; } else if (usingTRPC && !usingTw && usingAuth) { @@ -57,9 +78,36 @@ export const selectIndexFile = ({ indexFile = "with-tw.tsx"; } - if (indexFile !== "") { - const indexSrc = path.join(indexFileDir, indexFile); - const indexDest = path.join(projectDir, "src/pages/index.tsx"); - fs.copySync(indexSrc, indexDest); + const indexSrc = path.join(indexFileDir, indexFile); + const indexDest = path.join(projectDir, "src/pages/index.tsx"); + fs.copySync(indexSrc, indexDest); +}; + +// Similar to index, but for app router +export const selectPageFile = ({ + projectDir, + packages, +}: SelectBoilerplateProps) => { + const indexFileDir = path.join(PKG_ROOT, "template/extras/src/app/page"); + + const usingTRPC = packages.trpc.inUse; + const usingTw = packages.tailwind.inUse; + const usingAuth = packages.nextAuth.inUse; + + let indexFile = "base.tsx"; + if (usingTRPC && usingTw && usingAuth) { + indexFile = "with-auth-trpc-tw.tsx"; + } else if (usingTRPC && !usingTw && usingAuth) { + indexFile = "with-auth-trpc.tsx"; + } else if (usingTRPC && usingTw) { + indexFile = "with-trpc-tw.tsx"; + } else if (usingTRPC && !usingTw) { + indexFile = "with-trpc.tsx"; + } else if (!usingTRPC && usingTw) { + indexFile = "with-tw.tsx"; } + + const indexSrc = path.join(indexFileDir, indexFile); + const indexDest = path.join(projectDir, "src/app/page.tsx"); + fs.copySync(indexSrc, indexDest); }; diff --git a/cli/src/index.ts b/cli/src/index.ts index f945b04e20..ba7d16ae86 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -36,7 +36,7 @@ const main = async () => { const { appName, packages, - flags: { noGit, noInstall, importAlias }, + flags: { noGit, noInstall, importAlias, appRouter }, } = await runCli(); const usePackages = buildPkgInstallerMap(packages); @@ -48,8 +48,9 @@ const main = async () => { projectName: appDir, scopedAppName, packages: usePackages, - importAlias: importAlias, + importAlias, noInstall, + appRouter, }); // Write name to package.json diff --git a/cli/src/installers/dependencyVersionMap.ts b/cli/src/installers/dependencyVersionMap.ts index 11dc1f3877..7bfff4f9ce 100644 --- a/cli/src/installers/dependencyVersionMap.ts +++ b/cli/src/installers/dependencyVersionMap.ts @@ -16,6 +16,7 @@ export const dependencyVersionMap = { "drizzle-orm": "^0.28.5", "drizzle-kit": "^0.19.13", "dotenv-cli": "^7.3.0", + mysql2: "^3.6.1", "@planetscale/database": "^1.11.0", // TailwindCSS diff --git a/cli/src/installers/drizzle.ts b/cli/src/installers/drizzle.ts index f1de68e5d2..1b88e16ff6 100644 --- a/cli/src/installers/drizzle.ts +++ b/cli/src/installers/drizzle.ts @@ -13,7 +13,7 @@ export const drizzleInstaller: Installer = ({ }) => { addPackageDependency({ projectDir, - dependencies: ["drizzle-kit", "dotenv-cli"], + dependencies: ["drizzle-kit", "dotenv-cli", "mysql2"], devMode: true, }); addPackageDependency({ @@ -55,6 +55,7 @@ export const drizzleInstaller: Installer = ({ packageJsonContent.scripts = { ...packageJsonContent.scripts, "db:push": "dotenv drizzle-kit push:mysql", + "db:studio": "dotenv drizzle-kit studio", }; fs.copySync(configFile, configDest); diff --git a/cli/src/installers/index.ts b/cli/src/installers/index.ts index 45bef9ef14..8ab83c2c1b 100644 --- a/cli/src/installers/index.ts +++ b/cli/src/installers/index.ts @@ -23,6 +23,7 @@ export interface InstallerOptions { pkgManager: PackageManager; noInstall: boolean; packages?: PkgInstallerMap; + appRouter?: boolean; projectName: string; scopedAppName: string; } diff --git a/cli/src/installers/nextAuth.ts b/cli/src/installers/nextAuth.ts index f4a27ebdfc..3ac489fa71 100644 --- a/cli/src/installers/nextAuth.ts +++ b/cli/src/installers/nextAuth.ts @@ -6,7 +6,11 @@ import { type AvailableDependencies } from "~/installers/dependencyVersionMap.js import { type Installer } from "~/installers/index.js"; import { addPackageDependency } from "~/utils/addPackageDependency.js"; -export const nextAuthInstaller: Installer = ({ projectDir, packages }) => { +export const nextAuthInstaller: Installer = ({ + projectDir, + packages, + appRouter, +}) => { const usingPrisma = packages?.prisma.inUse; const usingDrizzle = packages?.drizzle.inUse; @@ -23,12 +27,16 @@ export const nextAuthInstaller: Installer = ({ projectDir, packages }) => { const extrasDir = path.join(PKG_ROOT, "template/extras"); const apiHandlerFile = "src/pages/api/auth/[...nextauth].ts"; - const apiHandlerSrc = path.join(extrasDir, apiHandlerFile); - const apiHandlerDest = path.join(projectDir, apiHandlerFile); + const routeHandlerFile = "src/app/api/auth/[...nextauth]/route.ts"; + const srcToUse = appRouter ? routeHandlerFile : apiHandlerFile; + + const apiHandlerSrc = path.join(extrasDir, srcToUse); + const apiHandlerDest = path.join(projectDir, srcToUse); const authConfigSrc = path.join( extrasDir, - "src/server/auth", + "src/server", + appRouter ? "auth-app" : "auth-pages", usingPrisma ? "with-prisma.ts" : usingDrizzle diff --git a/cli/src/installers/prisma.ts b/cli/src/installers/prisma.ts index b31b23b541..6001b3067d 100644 --- a/cli/src/installers/prisma.ts +++ b/cli/src/installers/prisma.ts @@ -38,6 +38,7 @@ export const prismaInstaller: Installer = ({ projectDir, packages }) => { ...packageJsonContent.scripts, postinstall: "prisma generate", "db:push": "prisma db push", + "db:studio": "prisma studio", }; fs.copySync(schemaSrc, schemaDest); diff --git a/cli/src/installers/tailwind.ts b/cli/src/installers/tailwind.ts index 9a1a4e497c..e55da469ea 100644 --- a/cli/src/installers/tailwind.ts +++ b/cli/src/installers/tailwind.ts @@ -36,8 +36,4 @@ export const tailwindInstaller: Installer = ({ projectDir }) => { fs.copySync(postcssCfgSrc, postcssCfgDest); fs.copySync(cssSrc, cssDest); fs.copySync(prettierSrc, prettierDest); - - // Remove vanilla css file - const indexModuleCss = path.join(projectDir, "src/pages/index.module.css"); - fs.unlinkSync(indexModuleCss); }; diff --git a/cli/src/installers/trpc.ts b/cli/src/installers/trpc.ts index 4d1cd24141..1f15d16d92 100644 --- a/cli/src/installers/trpc.ts +++ b/cli/src/installers/trpc.ts @@ -5,7 +5,11 @@ import { PKG_ROOT } from "~/consts.js"; import { type Installer } from "~/installers/index.js"; import { addPackageDependency } from "~/utils/addPackageDependency.js"; -export const trpcInstaller: Installer = ({ projectDir, packages }) => { +export const trpcInstaller: Installer = ({ + projectDir, + packages, + appRouter, +}) => { addPackageDependency({ projectDir, dependencies: [ @@ -26,11 +30,12 @@ export const trpcInstaller: Installer = ({ projectDir, packages }) => { const extrasDir = path.join(PKG_ROOT, "template/extras"); - const apiHandlerSrc = path.join(extrasDir, "src/pages/api/trpc/[trpc].ts"); - const apiHandlerDest = path.join(projectDir, "src/pages/api/trpc/[trpc].ts"); + const apiHandlerFile = "src/pages/api/trpc/[trpc].ts"; + const routeHandlerFile = "src/app/api/trpc/[trpc]/route.ts"; + const srcToUse = appRouter ? routeHandlerFile : apiHandlerFile; - const utilsSrc = path.join(extrasDir, "src/utils/api.ts"); - const utilsDest = path.join(projectDir, "src/utils/api.ts"); + const apiHandlerSrc = path.join(extrasDir, srcToUse); + const apiHandlerDest = path.join(projectDir, srcToUse); const trpcFile = usingAuth && usingDb @@ -40,7 +45,12 @@ export const trpcInstaller: Installer = ({ projectDir, packages }) => { : usingDb ? "with-db.ts" : "base.ts"; - const trpcSrc = path.join(extrasDir, "src/server/api/trpc", trpcFile); + const trpcSrc = path.join( + extrasDir, + "src/server/api", + appRouter ? "trpc-app" : "trpc-pages", + trpcFile + ); const trpcDest = path.join(projectDir, "src/server/api/trpc.ts"); const rootRouterSrc = path.join(extrasDir, "src/server/api/root.ts"); @@ -61,17 +71,52 @@ export const trpcInstaller: Installer = ({ projectDir, packages }) => { const exampleRouterSrc = path.join( extrasDir, - "src/server/api/routers/example", + "src/server/api/routers/post", exampleRouterFile ); const exampleRouterDest = path.join( projectDir, - "src/server/api/routers/example.ts" + "src/server/api/routers/post.ts" ); - fs.copySync(apiHandlerSrc, apiHandlerDest); - fs.copySync(utilsSrc, utilsDest); - fs.copySync(trpcSrc, trpcDest); - fs.copySync(rootRouterSrc, rootRouterDest); - fs.copySync(exampleRouterSrc, exampleRouterDest); + const copySrcDest: [string, string][] = [ + [apiHandlerSrc, apiHandlerDest], + [trpcSrc, trpcDest], + [rootRouterSrc, rootRouterDest], + [exampleRouterSrc, exampleRouterDest], + ]; + + if (appRouter) { + const trpcDir = path.join(extrasDir, "src/trpc"); + copySrcDest.push( + [ + path.join(trpcDir, "server.ts"), + path.join(projectDir, "src/trpc/server.ts"), + ], + [ + path.join(trpcDir, "react.tsx"), + path.join(projectDir, "src/trpc/react.tsx"), + ], + [ + path.join(trpcDir, "shared.ts"), + path.join(projectDir, "src/trpc/shared.ts"), + ], + [ + path.join( + extrasDir, + "src/app/_components", + packages?.tailwind.inUse ? "create-post-tw.tsx" : "create-post.tsx" + ), + path.join(projectDir, "src/app/_components/create-post.tsx"), + ] + ); + } else { + const utilsSrc = path.join(extrasDir, "src/utils/api.ts"); + const utilsDest = path.join(projectDir, "src/utils/api.ts"); + copySrcDest.push([utilsSrc, utilsDest]); + } + + copySrcDest.forEach(([src, dest]) => { + fs.copySync(src, dest); + }); }; diff --git a/cli/template/base/_eslintrc.cjs b/cli/template/base/_eslintrc.cjs index f15a4d583e..79cb511812 100644 --- a/cli/template/base/_eslintrc.cjs +++ b/cli/template/base/_eslintrc.cjs @@ -24,6 +24,12 @@ const config = { }, ], "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }], + "@typescript-eslint/no-misused-promises": [ + 2, + { + checksVoidReturn: { attributes: false }, + }, + ], }, }; diff --git a/cli/template/base/tsconfig.json b/cli/template/base/tsconfig.json index 03ebb748ae..1dfa3a89d1 100644 --- a/cli/template/base/tsconfig.json +++ b/cli/template/base/tsconfig.json @@ -19,7 +19,8 @@ "baseUrl": ".", "paths": { "~/*": ["./src/*"] - } + }, + "plugins": [{ "name": "next" }] }, "include": [ ".eslintrc.cjs", @@ -27,7 +28,8 @@ "**/*.ts", "**/*.tsx", "**/*.cjs", - "**/*.mjs" + "**/*.mjs", + ".next/types/**/*.ts" ], "exclude": ["node_modules"] } diff --git a/cli/template/extras/config/next-config-appdir.mjs b/cli/template/extras/config/next-config-appdir.mjs new file mode 100644 index 0000000000..0914d3133e --- /dev/null +++ b/cli/template/extras/config/next-config-appdir.mjs @@ -0,0 +1,10 @@ +/** + * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful + * for Docker builds. + */ +await import("./src/env.mjs"); + +/** @type {import("next").NextConfig} */ +const config = {}; + +export default config; diff --git a/cli/template/extras/config/tailwind.config.ts b/cli/template/extras/config/tailwind.config.ts index d4d3fa2959..f06488f911 100644 --- a/cli/template/extras/config/tailwind.config.ts +++ b/cli/template/extras/config/tailwind.config.ts @@ -1,9 +1,14 @@ import { type Config } from "tailwindcss"; +import { fontFamily } from "tailwindcss/defaultTheme"; export default { - content: ["./src/**/*.{js,ts,jsx,tsx}"], + content: ["./src/**/*.tsx"], theme: { - extend: {}, + extend: { + fontFamily: { + sans: ["var(--font-sans)", ...fontFamily.sans], + }, + }, }, plugins: [], } satisfies Config; diff --git a/cli/template/extras/prisma/schema/base.prisma b/cli/template/extras/prisma/schema/base.prisma index c229b415fb..ddb6e0995c 100644 --- a/cli/template/extras/prisma/schema/base.prisma +++ b/cli/template/extras/prisma/schema/base.prisma @@ -10,7 +10,7 @@ datasource db { url = env("DATABASE_URL") } -model Example { +model Post { id Int @id @default(autoincrement()) name String createdAt DateTime @default(now()) diff --git a/cli/template/extras/prisma/schema/with-auth.prisma b/cli/template/extras/prisma/schema/with-auth.prisma index 935817c6ba..848c86bc6e 100644 --- a/cli/template/extras/prisma/schema/with-auth.prisma +++ b/cli/template/extras/prisma/schema/with-auth.prisma @@ -14,12 +14,15 @@ datasource db { url = env("DATABASE_URL") } -model Example { +model Post { id Int @id @default(autoincrement()) name String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + createdBy User @relation(fields: [createdById], references: [id]) + createdById String + @@index([name]) } @@ -58,6 +61,7 @@ model User { image String? accounts Account[] sessions Session[] + posts Post[] } model VerificationToken { diff --git a/cli/template/extras/src/app/_components/create-post-tw.tsx b/cli/template/extras/src/app/_components/create-post-tw.tsx new file mode 100644 index 0000000000..02d7c382b8 --- /dev/null +++ b/cli/template/extras/src/app/_components/create-post-tw.tsx @@ -0,0 +1,43 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { useState } from "react"; + +import { api } from "~/trpc/react"; + +export function CreatePost() { + const router = useRouter(); + const [name, setName] = useState(""); + + const createPost = api.post.create.useMutation({ + onSuccess: () => { + router.refresh(); + setName(""); + }, + }); + + return ( +
{ + e.preventDefault(); + createPost.mutate({ name }); + }} + className="flex flex-col gap-2" + > + setName(e.target.value)} + className="w-full rounded-full px-4 py-2 text-black" + /> + +
+ ); +} diff --git a/cli/template/extras/src/app/_components/create-post.tsx b/cli/template/extras/src/app/_components/create-post.tsx new file mode 100644 index 0000000000..df7088c7b9 --- /dev/null +++ b/cli/template/extras/src/app/_components/create-post.tsx @@ -0,0 +1,44 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { useState } from "react"; + +import { api } from "~/trpc/react"; +import styles from "../index.module.css"; + +export function CreatePost() { + const router = useRouter(); + const [name, setName] = useState(""); + + const createPost = api.post.create.useMutation({ + onSuccess: () => { + router.refresh(); + setName(""); + }, + }); + + return ( +
{ + e.preventDefault(); + createPost.mutate({ name }); + }} + className={styles.form} + > + setName(e.target.value)} + className={styles.input} + /> + +
+ ); +} diff --git a/cli/template/extras/src/app/api/auth/[...nextauth]/route.ts b/cli/template/extras/src/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000000..7ef89677f2 --- /dev/null +++ b/cli/template/extras/src/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,7 @@ +import NextAuth from "next-auth"; + +import { authOptions } from "~/server/auth"; + +// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment +const handler = NextAuth(authOptions); +export { handler as GET, handler as POST }; diff --git a/cli/template/extras/src/app/api/trpc/[trpc]/route.ts b/cli/template/extras/src/app/api/trpc/[trpc]/route.ts new file mode 100644 index 0000000000..859f13a432 --- /dev/null +++ b/cli/template/extras/src/app/api/trpc/[trpc]/route.ts @@ -0,0 +1,24 @@ +import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; +import { type NextRequest } from "next/server"; + +import { env } from "~/env.mjs"; +import { appRouter } from "~/server/api/root"; +import { createTRPCContext } from "~/server/api/trpc"; + +const handler = (req: NextRequest) => + fetchRequestHandler({ + endpoint: "/api/trpc", + req, + router: appRouter, + createContext: () => createTRPCContext({ req }), + onError: + env.NODE_ENV === "development" + ? ({ path, error }) => { + console.error( + `❌ tRPC failed on ${path ?? ""}: ${error.message}` + ); + } + : undefined, + }); + +export { handler as GET, handler as POST }; diff --git a/cli/template/extras/src/app/layout/base.tsx b/cli/template/extras/src/app/layout/base.tsx new file mode 100644 index 0000000000..000740ecfb --- /dev/null +++ b/cli/template/extras/src/app/layout/base.tsx @@ -0,0 +1,25 @@ +import "~/styles/globals.css"; + +import { Inter } from "next/font/google"; + +const inter = Inter({ + subsets: ["latin"], +}); + +export const metadata = { + title: "Create T3 App", + description: "Generated by create-t3-app", + icons: [{ rel: "icon", url: "/favicon.ico" }], +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + {children} + + ); +} diff --git a/cli/template/extras/src/app/layout/with-trpc-tw.tsx b/cli/template/extras/src/app/layout/with-trpc-tw.tsx new file mode 100644 index 0000000000..9c1907b7e6 --- /dev/null +++ b/cli/template/extras/src/app/layout/with-trpc-tw.tsx @@ -0,0 +1,31 @@ +import "~/styles/globals.css"; + +import { Inter } from "next/font/google"; +import { headers } from "next/headers"; + +import { TRPCReactProvider } from "~/trpc/react"; + +const inter = Inter({ + subsets: ["latin"], + variable: "--font-sans", +}); + +export const metadata = { + title: "Create T3 App", + description: "Generated by create-t3-app", + icons: [{ rel: "icon", url: "/favicon.ico" }], +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + {children} + + + ); +} diff --git a/cli/template/extras/src/app/layout/with-trpc.tsx b/cli/template/extras/src/app/layout/with-trpc.tsx new file mode 100644 index 0000000000..bdd81493a6 --- /dev/null +++ b/cli/template/extras/src/app/layout/with-trpc.tsx @@ -0,0 +1,30 @@ +import "~/styles/globals.css"; + +import { Inter } from "next/font/google"; +import { headers } from "next/headers"; + +import { TRPCReactProvider } from "~/trpc/react"; + +const inter = Inter({ + subsets: ["latin"], +}); + +export const metadata = { + title: "Create T3 App", + description: "Generated by create-t3-app", + icons: [{ rel: "icon", url: "/favicon.ico" }], +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + {children} + + + ); +} diff --git a/cli/template/extras/src/app/layout/with-tw.tsx b/cli/template/extras/src/app/layout/with-tw.tsx new file mode 100644 index 0000000000..479d559c88 --- /dev/null +++ b/cli/template/extras/src/app/layout/with-tw.tsx @@ -0,0 +1,26 @@ +import "~/styles/globals.css"; + +import { Inter } from "next/font/google"; + +const inter = Inter({ + subsets: ["latin"], + variable: "--font-sans", +}); + +export const metadata = { + title: "Create T3 App", + description: "Generated by create-t3-app", + icons: [{ rel: "icon", url: "/favicon.ico" }], +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + {children} + + ); +} diff --git a/cli/template/extras/src/app/page/base.tsx b/cli/template/extras/src/app/page/base.tsx new file mode 100644 index 0000000000..7bc32eb602 --- /dev/null +++ b/cli/template/extras/src/app/page/base.tsx @@ -0,0 +1,39 @@ +import Link from "next/link"; + +import styles from "./index.module.css"; + +export default function Home() { + return ( +
+
+

+ Create T3 App +

+
+ +

First Steps →

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

Documentation →

+
+ Learn more about Create T3 App, the libraries it uses, and how to + deploy it. +
+ +
+
+
+ ); +} diff --git a/cli/template/extras/src/app/page/with-auth-trpc-tw.tsx b/cli/template/extras/src/app/page/with-auth-trpc-tw.tsx new file mode 100644 index 0000000000..83ee6e8387 --- /dev/null +++ b/cli/template/extras/src/app/page/with-auth-trpc-tw.tsx @@ -0,0 +1,82 @@ +import Link from "next/link"; + +import { CreatePost } from "~/app/_components/create-post"; +import { getServerAuthSession } from "~/server/auth"; +import { api } from "~/trpc/server"; + +export default async function Home() { + const hello = await api.post.hello.query({ text: "from tRPC" }); + const session = await getServerAuthSession(); + + return ( +
+
+

+ Create T3 App +

+
+ +

First Steps →

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

Documentation →

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

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

+ +
+

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

+ + {session ? "Sign out" : "Sign in"} + +
+
+ + +
+
+ ); +} + +async function CrudShowcase() { + const session = await getServerAuthSession(); + if (!session?.user) return null; + + const latestPost = await api.post.getLatest.query(); + + return ( +
+ {latestPost ? ( +

Your most recent post: {latestPost.name}

+ ) : ( +

You have no posts yet.

+ )} + + +
+ ); +} diff --git a/cli/template/extras/src/app/page/with-auth-trpc.tsx b/cli/template/extras/src/app/page/with-auth-trpc.tsx new file mode 100644 index 0000000000..b701be7283 --- /dev/null +++ b/cli/template/extras/src/app/page/with-auth-trpc.tsx @@ -0,0 +1,85 @@ +import Link from "next/link"; + +import { CreatePost } from "~/app/_components/create-post"; +import { getServerAuthSession } from "~/server/auth"; +import { api } from "~/trpc/server"; +import styles from "./index.module.css"; + +export default async function Home() { + const hello = await api.post.hello.query({ text: "from tRPC" }); + const session = await getServerAuthSession(); + + return ( +
+
+

+ Create T3 App +

+
+ +

First Steps →

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

Documentation →

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

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

+ +
+

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

+ + {session ? "Sign out" : "Sign in"} + +
+
+ + +
+
+ ); +} + +async function CrudShowcase() { + const session = await getServerAuthSession(); + if (!session?.user) return null; + + const latestPost = await api.post.getLatest.query(); + + return ( +
+ {latestPost ? ( +

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

+ ) : ( +

You have no posts yet.

+ )} + + +
+ ); +} diff --git a/cli/template/extras/src/app/page/with-trpc-tw.tsx b/cli/template/extras/src/app/page/with-trpc-tw.tsx new file mode 100644 index 0000000000..4a06b527b6 --- /dev/null +++ b/cli/template/extras/src/app/page/with-trpc-tw.tsx @@ -0,0 +1,65 @@ +import Link from "next/link"; + +import { CreatePost } from "~/app/_components/create-post"; +import { api } from "~/trpc/server"; + +export default async function Home() { + const hello = await api.post.hello.query({ text: "from tRPC" }); + + return ( +
+
+

+ Create T3 App +

+
+ +

First Steps →

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

Documentation →

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

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

+
+ + +
+
+ ); +} + +async function CrudShowcase() { + const latestPost = await api.post.getLatest.query(); + + return ( +
+ {latestPost ? ( +

Your most recent post: {latestPost.name}

+ ) : ( +

You have no posts yet.

+ )} + + +
+ ); +} diff --git a/cli/template/extras/src/app/page/with-trpc.tsx b/cli/template/extras/src/app/page/with-trpc.tsx new file mode 100644 index 0000000000..c8eea7f293 --- /dev/null +++ b/cli/template/extras/src/app/page/with-trpc.tsx @@ -0,0 +1,68 @@ +import Link from "next/link"; + +import { CreatePost } from "~/app/_components/create-post"; +import { api } from "~/trpc/server"; +import styles from "./index.module.css"; + +export default async function Home() { + const hello = await api.post.hello.query({ text: "from tRPC" }); + + return ( +
+
+

+ Create T3 App +

+
+ +

First Steps →

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

Documentation →

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

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

+
+ + +
+
+ ); +} + +async function CrudShowcase() { + const latestPost = await api.post.getLatest.query(); + + return ( +
+ {latestPost ? ( +

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

+ ) : ( +

You have no posts yet.

+ )} + + +
+ ); +} diff --git a/cli/template/extras/src/app/page/with-tw.tsx b/cli/template/extras/src/app/page/with-tw.tsx new file mode 100644 index 0000000000..91430b2207 --- /dev/null +++ b/cli/template/extras/src/app/page/with-tw.tsx @@ -0,0 +1,37 @@ +import Link from "next/link"; + +export default function HomePage() { + return ( +
+
+

+ Create T3 App +

+
+ +

First Steps →

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

Documentation →

+
+ Learn more about Create T3 App, the libraries it uses, and how to + deploy it. +
+ +
+
+
+ ); +} diff --git a/cli/template/base/src/pages/index.module.css b/cli/template/extras/src/index.module.css similarity index 82% rename from cli/template/base/src/pages/index.module.css rename to cli/template/extras/src/index.module.css index d9caeeaf2c..fac9982a3c 100644 --- a/cli/template/base/src/pages/index.module.css +++ b/cli/template/extras/src/index.module.css @@ -147,3 +147,31 @@ .loginButton:hover { background-color: rgb(255 255 255 / 0.2); } + +.form { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.input { + width: 100%; + border-radius: 9999px; + padding: 0.5rem 1rem; + color: black; +} + +.submitButton { + all: unset; + border-radius: 9999px; + background-color: rgb(255 255 255 / 0.1); + padding: 0.75rem 2.5rem; + font-weight: 600; + color: white; + text-align: center; + transition: background-color 150ms cubic-bezier(0.5, 0, 0.2, 1); +} + +.submitButton:hover { + background-color: rgb(255 255 255 / 0.2); +} diff --git a/cli/template/base/src/pages/_app.tsx b/cli/template/extras/src/pages/_app/base.tsx similarity index 100% rename from cli/template/base/src/pages/_app.tsx rename to cli/template/extras/src/pages/_app/base.tsx diff --git a/cli/template/base/src/pages/index.tsx b/cli/template/extras/src/pages/index/base.tsx similarity index 100% rename from cli/template/base/src/pages/index.tsx rename to cli/template/extras/src/pages/index/base.tsx diff --git a/cli/template/extras/src/pages/index/with-auth-trpc-tw.tsx b/cli/template/extras/src/pages/index/with-auth-trpc-tw.tsx index abaae58bf7..7f55cbfb00 100644 --- a/cli/template/extras/src/pages/index/with-auth-trpc-tw.tsx +++ b/cli/template/extras/src/pages/index/with-auth-trpc-tw.tsx @@ -5,7 +5,7 @@ import Link from "next/link"; import { api } from "~/utils/api"; export default function Home() { - const hello = api.example.hello.useQuery({ text: "from tRPC" }); + const hello = api.post.hello.useQuery({ text: "from tRPC" }); return ( <> @@ -58,7 +58,7 @@ export default function Home() { function AuthShowcase() { const { data: sessionData } = useSession(); - const { data: secretMessage } = api.example.getSecretMessage.useQuery( + const { data: secretMessage } = api.post.getSecretMessage.useQuery( undefined, // no input { enabled: sessionData?.user !== undefined } ); diff --git a/cli/template/extras/src/pages/index/with-auth-trpc.tsx b/cli/template/extras/src/pages/index/with-auth-trpc.tsx index a1f4dacbd8..6902f95c84 100644 --- a/cli/template/extras/src/pages/index/with-auth-trpc.tsx +++ b/cli/template/extras/src/pages/index/with-auth-trpc.tsx @@ -6,7 +6,7 @@ import { api } from "~/utils/api"; import styles from "./index.module.css"; export default function Home() { - const hello = api.example.hello.useQuery({ text: "from tRPC" }); + const hello = api.post.hello.useQuery({ text: "from tRPC" }); return ( <> @@ -59,7 +59,7 @@ export default function Home() { function AuthShowcase() { const { data: sessionData } = useSession(); - const { data: secretMessage } = api.example.getSecretMessage.useQuery( + const { data: secretMessage } = api.post.getSecretMessage.useQuery( undefined, // no input { enabled: sessionData?.user !== undefined } ); diff --git a/cli/template/extras/src/pages/index/with-trpc-tw.tsx b/cli/template/extras/src/pages/index/with-trpc-tw.tsx index 8684e4b633..26341d62db 100644 --- a/cli/template/extras/src/pages/index/with-trpc-tw.tsx +++ b/cli/template/extras/src/pages/index/with-trpc-tw.tsx @@ -4,7 +4,7 @@ import Link from "next/link"; import { api } from "~/utils/api"; export default function Home() { - const hello = api.example.hello.useQuery({ text: "from tRPC" }); + const hello = api.post.hello.useQuery({ text: "from tRPC" }); return ( <> diff --git a/cli/template/extras/src/pages/index/with-trpc.tsx b/cli/template/extras/src/pages/index/with-trpc.tsx index 9b30a9b7bd..a89dd17103 100644 --- a/cli/template/extras/src/pages/index/with-trpc.tsx +++ b/cli/template/extras/src/pages/index/with-trpc.tsx @@ -5,7 +5,7 @@ import { api } from "~/utils/api"; import styles from "./index.module.css"; export default function Home() { - const hello = api.example.hello.useQuery({ text: "from tRPC" }); + const hello = api.post.hello.useQuery({ text: "from tRPC" }); return ( <> diff --git a/cli/template/extras/src/server/api/root.ts b/cli/template/extras/src/server/api/root.ts index 7caea0fed0..3d629a7a5b 100644 --- a/cli/template/extras/src/server/api/root.ts +++ b/cli/template/extras/src/server/api/root.ts @@ -1,4 +1,4 @@ -import { exampleRouter } from "~/server/api/routers/example"; +import { postRouter } from "~/server/api/routers/post"; import { createTRPCRouter } from "~/server/api/trpc"; /** @@ -7,7 +7,7 @@ import { createTRPCRouter } from "~/server/api/trpc"; * All routers added in /api/routers should be manually added here. */ export const appRouter = createTRPCRouter({ - example: exampleRouter, + post: postRouter, }); // export type definition of API diff --git a/cli/template/extras/src/server/api/routers/example/base.ts b/cli/template/extras/src/server/api/routers/example/base.ts deleted file mode 100644 index 1c70bb2840..0000000000 --- a/cli/template/extras/src/server/api/routers/example/base.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { z } from "zod"; - -import { createTRPCRouter, publicProcedure } from "~/server/api/trpc"; - -export const exampleRouter = createTRPCRouter({ - hello: publicProcedure - .input(z.object({ text: z.string() })) - .query(({ input }) => { - return { - greeting: `Hello ${input.text}`, - }; - }), -}); diff --git a/cli/template/extras/src/server/api/routers/example/with-auth-drizzle.ts b/cli/template/extras/src/server/api/routers/example/with-auth-drizzle.ts deleted file mode 100644 index 55a3189509..0000000000 --- a/cli/template/extras/src/server/api/routers/example/with-auth-drizzle.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { z } from "zod"; - -import { - createTRPCRouter, - protectedProcedure, - publicProcedure, -} from "~/server/api/trpc"; - -export const exampleRouter = createTRPCRouter({ - hello: publicProcedure - .input(z.object({ text: z.string() })) - .query(({ input }) => { - return { - greeting: `Hello ${input.text}`, - }; - }), - - getAll: publicProcedure.query(({ ctx }) => { - return ctx.db.query.example.findMany(); - }), - - getSecretMessage: protectedProcedure.query(() => { - return "you can now see this secret message!"; - }), -}); diff --git a/cli/template/extras/src/server/api/routers/example/with-auth-prisma.ts b/cli/template/extras/src/server/api/routers/example/with-auth-prisma.ts deleted file mode 100644 index ed0ce35ba1..0000000000 --- a/cli/template/extras/src/server/api/routers/example/with-auth-prisma.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { z } from "zod"; - -import { - createTRPCRouter, - protectedProcedure, - publicProcedure, -} from "~/server/api/trpc"; - -export const exampleRouter = createTRPCRouter({ - hello: publicProcedure - .input(z.object({ text: z.string() })) - .query(({ input }) => { - return { - greeting: `Hello ${input.text}`, - }; - }), - - getAll: publicProcedure.query(({ ctx }) => { - return ctx.db.example.findMany(); - }), - - getSecretMessage: protectedProcedure.query(() => { - return "you can now see this secret message!"; - }), -}); diff --git a/cli/template/extras/src/server/api/routers/example/with-auth.ts b/cli/template/extras/src/server/api/routers/example/with-auth.ts deleted file mode 100644 index 035cd778a6..0000000000 --- a/cli/template/extras/src/server/api/routers/example/with-auth.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { z } from "zod"; - -import { - createTRPCRouter, - protectedProcedure, - publicProcedure, -} from "~/server/api/trpc"; - -export const exampleRouter = createTRPCRouter({ - hello: publicProcedure - .input(z.object({ text: z.string() })) - .query(({ input }) => { - return { - greeting: `Hello ${input.text}`, - }; - }), - - getSecretMessage: protectedProcedure.query(() => { - return "you can now see this secret message!"; - }), -}); diff --git a/cli/template/extras/src/server/api/routers/example/with-drizzle.ts b/cli/template/extras/src/server/api/routers/example/with-drizzle.ts deleted file mode 100644 index 15f90c76b6..0000000000 --- a/cli/template/extras/src/server/api/routers/example/with-drizzle.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { z } from "zod"; - -import { createTRPCRouter, publicProcedure } from "~/server/api/trpc"; - -export const exampleRouter = createTRPCRouter({ - hello: publicProcedure - .input(z.object({ text: z.string() })) - .query(({ input }) => { - return { - greeting: `Hello ${input.text}`, - }; - }), - getAll: publicProcedure.query(({ ctx }) => { - return ctx.db.query.example.findMany(); - }), -}); diff --git a/cli/template/extras/src/server/api/routers/example/with-prisma.ts b/cli/template/extras/src/server/api/routers/example/with-prisma.ts deleted file mode 100644 index de5d5c5b6e..0000000000 --- a/cli/template/extras/src/server/api/routers/example/with-prisma.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { z } from "zod"; - -import { createTRPCRouter, publicProcedure } from "~/server/api/trpc"; - -export const exampleRouter = createTRPCRouter({ - hello: publicProcedure - .input(z.object({ text: z.string() })) - .query(({ input }) => { - return { - greeting: `Hello ${input.text}`, - }; - }), - getAll: publicProcedure.query(({ ctx }) => { - return ctx.db.example.findMany(); - }), -}); diff --git a/cli/template/extras/src/server/api/routers/post/base.ts b/cli/template/extras/src/server/api/routers/post/base.ts new file mode 100644 index 0000000000..1673517554 --- /dev/null +++ b/cli/template/extras/src/server/api/routers/post/base.ts @@ -0,0 +1,32 @@ +import { z } from "zod"; + +import { createTRPCRouter, publicProcedure } from "~/server/api/trpc"; + +let post = { + id: 1, + name: "Hello World", +}; + +export const postRouter = createTRPCRouter({ + hello: publicProcedure + .input(z.object({ text: z.string() })) + .query(({ input }) => { + return { + greeting: `Hello ${input.text}`, + }; + }), + + create: publicProcedure + .input(z.object({ name: z.string().min(1) })) + .mutation(async ({ input }) => { + // simulate a slow db call + await new Promise((resolve) => setTimeout(resolve, 1000)); + + post = { id: post.id + 1, name: input.name }; + return post; + }), + + getLatest: publicProcedure.query(() => { + return post; + }), +}); diff --git a/cli/template/extras/src/server/api/routers/post/with-auth-drizzle.ts b/cli/template/extras/src/server/api/routers/post/with-auth-drizzle.ts new file mode 100644 index 0000000000..1fc4a3c486 --- /dev/null +++ b/cli/template/extras/src/server/api/routers/post/with-auth-drizzle.ts @@ -0,0 +1,40 @@ +import { z } from "zod"; + +import { + createTRPCRouter, + protectedProcedure, + publicProcedure, +} from "~/server/api/trpc"; +import { posts } from "~/server/db/schema"; + +export const postRouter = createTRPCRouter({ + hello: publicProcedure + .input(z.object({ text: z.string() })) + .query(({ input }) => { + return { + greeting: `Hello ${input.text}`, + }; + }), + + create: protectedProcedure + .input(z.object({ name: z.string().min(1) })) + .mutation(async ({ ctx, input }) => { + // simulate a slow db call + await new Promise((resolve) => setTimeout(resolve, 1000)); + + await ctx.db.insert(posts).values({ + name: input.name, + createdById: ctx.session.user.id, + }); + }), + + getLatest: publicProcedure.query(({ ctx }) => { + return ctx.db.query.posts.findFirst({ + orderBy: (posts, { desc }) => [desc(posts.createdAt)], + }); + }), + + getSecretMessage: protectedProcedure.query(() => { + return "you can now see this secret message!"; + }), +}); diff --git a/cli/template/extras/src/server/api/routers/post/with-auth-prisma.ts b/cli/template/extras/src/server/api/routers/post/with-auth-prisma.ts new file mode 100644 index 0000000000..3994691ed1 --- /dev/null +++ b/cli/template/extras/src/server/api/routers/post/with-auth-prisma.ts @@ -0,0 +1,42 @@ +import { z } from "zod"; + +import { + createTRPCRouter, + protectedProcedure, + publicProcedure, +} from "~/server/api/trpc"; + +export const postRouter = createTRPCRouter({ + hello: publicProcedure + .input(z.object({ text: z.string() })) + .query(({ input }) => { + return { + greeting: `Hello ${input.text}`, + }; + }), + + create: protectedProcedure + .input(z.object({ name: z.string().min(1) })) + .mutation(async ({ ctx, input }) => { + // simulate a slow db call + await new Promise((resolve) => setTimeout(resolve, 1000)); + + return ctx.db.post.create({ + data: { + name: input.name, + createdBy: { connect: { id: ctx.session.user.id } }, + }, + }); + }), + + getLatest: protectedProcedure.query(({ ctx }) => { + return ctx.db.post.findFirst({ + orderBy: { createdAt: "desc" }, + where: { createdBy: { id: ctx.session.user.id } }, + }); + }), + + getSecretMessage: protectedProcedure.query(() => { + return "you can now see this secret message!"; + }), +}); diff --git a/cli/template/extras/src/server/api/routers/post/with-auth.ts b/cli/template/extras/src/server/api/routers/post/with-auth.ts new file mode 100644 index 0000000000..a2072d3116 --- /dev/null +++ b/cli/template/extras/src/server/api/routers/post/with-auth.ts @@ -0,0 +1,40 @@ +import { z } from "zod"; + +import { + createTRPCRouter, + protectedProcedure, + publicProcedure, +} from "~/server/api/trpc"; + +let post = { + id: 1, + name: "Hello World", +}; + +export const postRouter = createTRPCRouter({ + hello: publicProcedure + .input(z.object({ text: z.string() })) + .query(({ input }) => { + return { + greeting: `Hello ${input.text}`, + }; + }), + + create: protectedProcedure + .input(z.object({ name: z.string().min(1) })) + .mutation(async ({ input }) => { + // simulate a slow db call + await new Promise((resolve) => setTimeout(resolve, 1000)); + + post = { id: post.id + 1, name: input.name }; + return post; + }), + + getLatest: protectedProcedure.query(() => { + return post; + }), + + getSecretMessage: protectedProcedure.query(() => { + return "you can now see this secret message!"; + }), +}); diff --git a/cli/template/extras/src/server/api/routers/post/with-drizzle.ts b/cli/template/extras/src/server/api/routers/post/with-drizzle.ts new file mode 100644 index 0000000000..b8e95d2939 --- /dev/null +++ b/cli/template/extras/src/server/api/routers/post/with-drizzle.ts @@ -0,0 +1,31 @@ +import { z } from "zod"; + +import { createTRPCRouter, publicProcedure } from "~/server/api/trpc"; +import { posts } from "~/server/db/schema"; + +export const postRouter = createTRPCRouter({ + hello: publicProcedure + .input(z.object({ text: z.string() })) + .query(({ input }) => { + return { + greeting: `Hello ${input.text}`, + }; + }), + + create: publicProcedure + .input(z.object({ name: z.string().min(1) })) + .mutation(async ({ ctx, input }) => { + // simulate a slow db call + await new Promise((resolve) => setTimeout(resolve, 1000)); + + await ctx.db.insert(posts).values({ + name: input.name, + }); + }), + + getLatest: publicProcedure.query(({ ctx }) => { + return ctx.db.query.posts.findFirst({ + orderBy: (posts, { desc }) => [desc(posts.createdAt)], + }); + }), +}); diff --git a/cli/template/extras/src/server/api/routers/post/with-prisma.ts b/cli/template/extras/src/server/api/routers/post/with-prisma.ts new file mode 100644 index 0000000000..68367a35b6 --- /dev/null +++ b/cli/template/extras/src/server/api/routers/post/with-prisma.ts @@ -0,0 +1,32 @@ +import { z } from "zod"; + +import { createTRPCRouter, publicProcedure } from "~/server/api/trpc"; + +export const postRouter = createTRPCRouter({ + hello: publicProcedure + .input(z.object({ text: z.string() })) + .query(({ input }) => { + return { + greeting: `Hello ${input.text}`, + }; + }), + + create: publicProcedure + .input(z.object({ name: z.string().min(1) })) + .mutation(async ({ ctx, input }) => { + // simulate a slow db call + await new Promise((resolve) => setTimeout(resolve, 1000)); + + return ctx.db.post.create({ + data: { + name: input.name, + }, + }); + }), + + getLatest: publicProcedure.query(({ ctx }) => { + return ctx.db.post.findFirst({ + orderBy: { createdAt: "desc" }, + }); + }), +}); diff --git a/cli/template/extras/src/server/api/trpc-app/base.ts b/cli/template/extras/src/server/api/trpc-app/base.ts new file mode 100644 index 0000000000..8984c3063c --- /dev/null +++ b/cli/template/extras/src/server/api/trpc-app/base.ts @@ -0,0 +1,99 @@ +/** + * YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS: + * 1. You want to modify request context (see Part 1). + * 2. You want to create a new middleware or type of procedure (see Part 3). + * + * TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will + * need to use are documented accordingly near the end. + */ +import { initTRPC } from "@trpc/server"; +import { type NextRequest } from "next/server"; +import superjson from "superjson"; +import { ZodError } from "zod"; + +/** + * 1. CONTEXT + * + * This section defines the "contexts" that are available in the backend API. + * + * These allow you to access things when processing a request, like the database, the session, etc. + */ + +interface CreateContextOptions { + headers: Headers; +} + +/** + * This helper generates the "internals" for a tRPC context. If you need to use it, you can export + * it from here. + * + * Examples of things you may need it for: + * - testing, so we don't have to mock Next.js' req/res + * - tRPC's `createSSGHelpers`, where we don't have req/res + * + * @see https://create.t3.gg/en/usage/trpc#-serverapitrpcts + */ +export const createInnerTRPCContext = (opts: CreateContextOptions) => { + return { + headers: opts.headers, + }; +}; + +/** + * This is the actual context you will use in your router. It will be used to process every request + * that goes through your tRPC endpoint. + * + * @see https://trpc.io/docs/context + */ +export const createTRPCContext = (opts: { req: NextRequest }) => { + // Fetch stuff that depends on the request + + return createInnerTRPCContext({ + headers: opts.req.headers, + }); +}; + +/** + * 2. INITIALIZATION + * + * This is where the tRPC API is initialized, connecting the context and transformer. We also parse + * ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation + * errors on the backend. + */ + +const t = initTRPC.context().create({ + transformer: superjson, + errorFormatter({ shape, error }) { + return { + ...shape, + data: { + ...shape.data, + zodError: + error.cause instanceof ZodError ? error.cause.flatten() : null, + }, + }; + }, +}); + +/** + * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT) + * + * These are the pieces you use to build your tRPC API. You should import these a lot in the + * "/src/server/api/routers" directory. + */ + +/** + * This is how you create new routers and sub-routers in your tRPC API. + * + * @see https://trpc.io/docs/router + */ +export const createTRPCRouter = t.router; + +/** + * Public (unauthenticated) procedure + * + * This is the base piece you use to build new queries and mutations on your tRPC API. It does not + * guarantee that a user querying is authorized, but you can still access user session data if they + * are logged in. + */ +export const publicProcedure = t.procedure; diff --git a/cli/template/extras/src/server/api/trpc-app/with-auth-db.ts b/cli/template/extras/src/server/api/trpc-app/with-auth-db.ts new file mode 100644 index 0000000000..37cc16a5c5 --- /dev/null +++ b/cli/template/extras/src/server/api/trpc-app/with-auth-db.ts @@ -0,0 +1,130 @@ +/** + * YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS: + * 1. You want to modify request context (see Part 1). + * 2. You want to create a new middleware or type of procedure (see Part 3). + * + * TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will + * need to use are documented accordingly near the end. + */ + +import { initTRPC, TRPCError } from "@trpc/server"; +import { type NextRequest } from "next/server"; +import superjson from "superjson"; +import { ZodError } from "zod"; + +import { getServerAuthSession } from "~/server/auth"; +import { db } from "~/server/db"; + +/** + * 1. CONTEXT + * + * This section defines the "contexts" that are available in the backend API. + * + * These allow you to access things when processing a request, like the database, the session, etc. + */ + +interface CreateContextOptions { + headers: Headers; +} + +/** + * This helper generates the "internals" for a tRPC context. If you need to use it, you can export + * it from here. + * + * Examples of things you may need it for: + * - testing, so we don't have to mock Next.js' req/res + * - tRPC's `createSSGHelpers`, where we don't have req/res + * + * @see https://create.t3.gg/en/usage/trpc#-serverapitrpcts + */ +export const createInnerTRPCContext = async (opts: CreateContextOptions) => { + const session = await getServerAuthSession(); + + return { + session, + headers: opts.headers, + db, + }; +}; + +/** + * This is the actual context you will use in your router. It will be used to process every request + * that goes through your tRPC endpoint. + * + * @see https://trpc.io/docs/context + */ +export const createTRPCContext = async (opts: { req: NextRequest }) => { + // Fetch stuff that depends on the request + + return await createInnerTRPCContext({ + headers: opts.req.headers, + }); +}; + +/** + * 2. INITIALIZATION + * + * This is where the tRPC API is initialized, connecting the context and transformer. We also parse + * ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation + * errors on the backend. + */ + +const t = initTRPC.context().create({ + transformer: superjson, + errorFormatter({ shape, error }) { + return { + ...shape, + data: { + ...shape.data, + zodError: + error.cause instanceof ZodError ? error.cause.flatten() : null, + }, + }; + }, +}); + +/** + * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT) + * + * These are the pieces you use to build your tRPC API. You should import these a lot in the + * "/src/server/api/routers" directory. + */ + +/** + * This is how you create new routers and sub-routers in your tRPC API. + * + * @see https://trpc.io/docs/router + */ +export const createTRPCRouter = t.router; + +/** + * Public (unauthenticated) procedure + * + * This is the base piece you use to build new queries and mutations on your tRPC API. It does not + * guarantee that a user querying is authorized, but you can still access user session data if they + * are logged in. + */ +export const publicProcedure = t.procedure; + +/** Reusable middleware that enforces users are logged in before running the procedure. */ +const enforceUserIsAuthed = t.middleware(({ ctx, next }) => { + if (!ctx.session || !ctx.session.user) { + throw new TRPCError({ code: "UNAUTHORIZED" }); + } + return next({ + ctx: { + // infers the `session` as non-nullable + session: { ...ctx.session, user: ctx.session.user }, + }, + }); +}); + +/** + * Protected (authenticated) procedure + * + * If you want a query or mutation to ONLY be accessible to logged in users, use this. It verifies + * the session is valid and guarantees `ctx.session.user` is not null. + * + * @see https://trpc.io/docs/procedures + */ +export const protectedProcedure = t.procedure.use(enforceUserIsAuthed); diff --git a/cli/template/extras/src/server/api/trpc-app/with-auth.ts b/cli/template/extras/src/server/api/trpc-app/with-auth.ts new file mode 100644 index 0000000000..a865577dc5 --- /dev/null +++ b/cli/template/extras/src/server/api/trpc-app/with-auth.ts @@ -0,0 +1,127 @@ +/** + * YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS: + * 1. You want to modify request context (see Part 1). + * 2. You want to create a new middleware or type of procedure (see Part 3). + * + * TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will + * need to use are documented accordingly near the end. + */ +import { initTRPC, TRPCError } from "@trpc/server"; +import { type NextRequest } from "next/server"; +import superjson from "superjson"; +import { ZodError } from "zod"; + +import { getServerAuthSession } from "~/server/auth"; + +/** + * 1. CONTEXT + * + * This section defines the "contexts" that are available in the backend API. + * + * These allow you to access things when processing a request, like the database, the session, etc. + */ + +interface CreateContextOptions { + headers: Headers; +} + +/** + * This helper generates the "internals" for a tRPC context. If you need to use it, you can export + * it from here. + * + * Examples of things you may need it for: + * - testing, so we don't have to mock Next.js' req/res + * - tRPC's `createSSGHelpers`, where we don't have req/res + * + * @see https://create.t3.gg/en/usage/trpc#-serverapitrpcts + */ +export const createInnerTRPCContext = async (opts: CreateContextOptions) => { + const session = await getServerAuthSession(); + + return { + session, + headers: opts.headers, + }; +}; + +/** + * This is the actual context you will use in your router. It will be used to process every request + * that goes through your tRPC endpoint. + * + * @see https://trpc.io/docs/context + */ +export const createTRPCContext = async (opts: { req: NextRequest }) => { + // Fetch stuff that depends on the request + + return await createInnerTRPCContext({ + headers: opts.req.headers, + }); +}; + +/** + * 2. INITIALIZATION + * + * This is where the tRPC API is initialized, connecting the context and transformer. We also parse + * ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation + * errors on the backend. + */ + +const t = initTRPC.context().create({ + transformer: superjson, + errorFormatter({ shape, error }) { + return { + ...shape, + data: { + ...shape.data, + zodError: + error.cause instanceof ZodError ? error.cause.flatten() : null, + }, + }; + }, +}); + +/** + * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT) + * + * These are the pieces you use to build your tRPC API. You should import these a lot in the + * "/src/server/api/routers" directory. + */ + +/** + * This is how you create new routers and sub-routers in your tRPC API. + * + * @see https://trpc.io/docs/router + */ +export const createTRPCRouter = t.router; + +/** + * Public (unauthenticated) procedure + * + * This is the base piece you use to build new queries and mutations on your tRPC API. It does not + * guarantee that a user querying is authorized, but you can still access user session data if they + * are logged in. + */ +export const publicProcedure = t.procedure; + +/** Reusable middleware that enforces users are logged in before running the procedure. */ +const enforceUserIsAuthed = t.middleware(({ ctx, next }) => { + if (!ctx.session || !ctx.session.user) { + throw new TRPCError({ code: "UNAUTHORIZED" }); + } + return next({ + ctx: { + // infers the `session` as non-nullable + session: { ...ctx.session, user: ctx.session.user }, + }, + }); +}); + +/** + * Protected (authenticated) procedure + * + * If you want a query or mutation to ONLY be accessible to logged in users, use this. It verifies + * the session is valid and guarantees `ctx.session.user` is not null. + * + * @see https://trpc.io/docs/procedures + */ +export const protectedProcedure = t.procedure.use(enforceUserIsAuthed); diff --git a/cli/template/extras/src/server/api/trpc-app/with-db.ts b/cli/template/extras/src/server/api/trpc-app/with-db.ts new file mode 100644 index 0000000000..0dceca843a --- /dev/null +++ b/cli/template/extras/src/server/api/trpc-app/with-db.ts @@ -0,0 +1,102 @@ +/** + * YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS: + * 1. You want to modify request context (see Part 1). + * 2. You want to create a new middleware or type of procedure (see Part 3). + * + * TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will + * need to use are documented accordingly near the end. + */ +import { initTRPC } from "@trpc/server"; +import { type NextRequest } from "next/server"; +import superjson from "superjson"; +import { ZodError } from "zod"; + +import { db } from "~/server/db"; + +/** + * 1. CONTEXT + * + * This section defines the "contexts" that are available in the backend API. + * + * These allow you to access things when processing a request, like the database, the session, etc. + */ + +interface CreateContextOptions { + headers: Headers; +} + +/** + * This helper generates the "internals" for a tRPC context. If you need to use it, you can export + * it from here. + * + * Examples of things you may need it for: + * - testing, so we don't have to mock Next.js' req/res + * - tRPC's `createSSGHelpers`, where we don't have req/res + * + * @see https://create.t3.gg/en/usage/trpc#-serverapitrpcts + */ +export const createInnerTRPCContext = (opts: CreateContextOptions) => { + return { + headers: opts.headers, + db, + }; +}; + +/** + * This is the actual context you will use in your router. It will be used to process every request + * that goes through your tRPC endpoint. + * + * @see https://trpc.io/docs/context + */ +export const createTRPCContext = (opts: { req: NextRequest }) => { + // Fetch stuff that depends on the request + + return createInnerTRPCContext({ + headers: opts.req.headers, + }); +}; + +/** + * 2. INITIALIZATION + * + * This is where the tRPC API is initialized, connecting the context and transformer. We also parse + * ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation + * errors on the backend. + */ + +const t = initTRPC.context().create({ + transformer: superjson, + errorFormatter({ shape, error }) { + return { + ...shape, + data: { + ...shape.data, + zodError: + error.cause instanceof ZodError ? error.cause.flatten() : null, + }, + }; + }, +}); + +/** + * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT) + * + * These are the pieces you use to build your tRPC API. You should import these a lot in the + * "/src/server/api/routers" directory. + */ + +/** + * This is how you create new routers and sub-routers in your tRPC API. + * + * @see https://trpc.io/docs/router + */ +export const createTRPCRouter = t.router; + +/** + * Public (unauthenticated) procedure + * + * This is the base piece you use to build new queries and mutations on your tRPC API. It does not + * guarantee that a user querying is authorized, but you can still access user session data if they + * are logged in. + */ +export const publicProcedure = t.procedure; diff --git a/cli/template/extras/src/server/api/trpc/base.ts b/cli/template/extras/src/server/api/trpc-pages/base.ts similarity index 100% rename from cli/template/extras/src/server/api/trpc/base.ts rename to cli/template/extras/src/server/api/trpc-pages/base.ts diff --git a/cli/template/extras/src/server/api/trpc/with-auth-db.ts b/cli/template/extras/src/server/api/trpc-pages/with-auth-db.ts similarity index 100% rename from cli/template/extras/src/server/api/trpc/with-auth-db.ts rename to cli/template/extras/src/server/api/trpc-pages/with-auth-db.ts diff --git a/cli/template/extras/src/server/api/trpc/with-auth.ts b/cli/template/extras/src/server/api/trpc-pages/with-auth.ts similarity index 100% rename from cli/template/extras/src/server/api/trpc/with-auth.ts rename to cli/template/extras/src/server/api/trpc-pages/with-auth.ts diff --git a/cli/template/extras/src/server/api/trpc/with-db.ts b/cli/template/extras/src/server/api/trpc-pages/with-db.ts similarity index 100% rename from cli/template/extras/src/server/api/trpc/with-db.ts rename to cli/template/extras/src/server/api/trpc-pages/with-db.ts diff --git a/cli/template/extras/src/server/auth-app/base.ts b/cli/template/extras/src/server/auth-app/base.ts new file mode 100644 index 0000000000..41f6c2319a --- /dev/null +++ b/cli/template/extras/src/server/auth-app/base.ts @@ -0,0 +1,68 @@ +import { + getServerSession, + type DefaultSession, + type NextAuthOptions, +} from "next-auth"; +import DiscordProvider from "next-auth/providers/discord"; + +import { env } from "~/env.mjs"; + +/** + * Module augmentation for `next-auth` types. Allows us to add custom properties to the `session` + * object and keep type safety. + * + * @see https://next-auth.js.org/getting-started/typescript#module-augmentation + */ +declare module "next-auth" { + interface Session extends DefaultSession { + user: { + id: string; + // ...other properties + // role: UserRole; + } & DefaultSession["user"]; + } + + // interface User { + // // ...other properties + // // role: UserRole; + // } +} + +/** + * Options for NextAuth.js used to configure adapters, providers, callbacks, etc. + * + * @see https://next-auth.js.org/configuration/options + */ +export const authOptions: NextAuthOptions = { + callbacks: { + session: ({ session, token }) => ({ + ...session, + user: { + ...session.user, + id: token.sub, + }, + }), + }, + providers: [ + DiscordProvider({ + clientId: env.DISCORD_CLIENT_ID, + clientSecret: env.DISCORD_CLIENT_SECRET, + }), + /** + * ...add more providers here. + * + * Most other providers require a bit more work than the Discord provider. For example, the + * GitHub provider requires you to add the `refresh_token_expires_in` field to the Account + * model. Refer to the NextAuth.js docs for the provider you want to use. Example: + * + * @see https://next-auth.js.org/providers/github + */ + ], +}; + +/** + * Wrapper for `getServerSession` so that you don't need to import the `authOptions` in every file. + * + * @see https://next-auth.js.org/configuration/nextjs + */ +export const getServerAuthSession = () => getServerSession(authOptions); diff --git a/cli/template/extras/src/server/auth-app/with-drizzle.ts b/cli/template/extras/src/server/auth-app/with-drizzle.ts new file mode 100644 index 0000000000..6809dbd1c0 --- /dev/null +++ b/cli/template/extras/src/server/auth-app/with-drizzle.ts @@ -0,0 +1,72 @@ +import { DrizzleAdapter } from "@auth/drizzle-adapter"; +import { + getServerSession, + type DefaultSession, + type NextAuthOptions, +} from "next-auth"; +import DiscordProvider from "next-auth/providers/discord"; + +import { env } from "~/env.mjs"; +import { db } from "~/server/db"; +import { mysqlTable } from "~/server/db/schema"; + +/** + * Module augmentation for `next-auth` types. Allows us to add custom properties to the `session` + * object and keep type safety. + * + * @see https://next-auth.js.org/getting-started/typescript#module-augmentation + */ +declare module "next-auth" { + interface Session extends DefaultSession { + user: { + id: string; + // ...other properties + // role: UserRole; + } & DefaultSession["user"]; + } + + // interface User { + // // ...other properties + // // role: UserRole; + // } +} + +/** + * Options for NextAuth.js used to configure adapters, providers, callbacks, etc. + * + * @see https://next-auth.js.org/configuration/options + */ +export const authOptions: NextAuthOptions = { + callbacks: { + session: ({ session, user }) => ({ + ...session, + user: { + ...session.user, + id: user.id, + }, + }), + }, + adapter: DrizzleAdapter(db, mysqlTable), + providers: [ + DiscordProvider({ + clientId: env.DISCORD_CLIENT_ID, + clientSecret: env.DISCORD_CLIENT_SECRET, + }), + /** + * ...add more providers here. + * + * Most other providers require a bit more work than the Discord provider. For example, the + * GitHub provider requires you to add the `refresh_token_expires_in` field to the Account + * model. Refer to the NextAuth.js docs for the provider you want to use. Example: + * + * @see https://next-auth.js.org/providers/github + */ + ], +}; + +/** + * Wrapper for `getServerSession` so that you don't need to import the `authOptions` in every file. + * + * @see https://next-auth.js.org/configuration/nextjs + */ +export const getServerAuthSession = () => getServerSession(authOptions); diff --git a/cli/template/extras/src/server/auth-app/with-prisma.ts b/cli/template/extras/src/server/auth-app/with-prisma.ts new file mode 100644 index 0000000000..8ac0ed24ed --- /dev/null +++ b/cli/template/extras/src/server/auth-app/with-prisma.ts @@ -0,0 +1,71 @@ +import { PrismaAdapter } from "@next-auth/prisma-adapter"; +import { + getServerSession, + type DefaultSession, + type NextAuthOptions, +} from "next-auth"; +import DiscordProvider from "next-auth/providers/discord"; + +import { env } from "~/env.mjs"; +import { db } from "~/server/db"; + +/** + * Module augmentation for `next-auth` types. Allows us to add custom properties to the `session` + * object and keep type safety. + * + * @see https://next-auth.js.org/getting-started/typescript#module-augmentation + */ +declare module "next-auth" { + interface Session extends DefaultSession { + user: { + id: string; + // ...other properties + // role: UserRole; + } & DefaultSession["user"]; + } + + // interface User { + // // ...other properties + // // role: UserRole; + // } +} + +/** + * Options for NextAuth.js used to configure adapters, providers, callbacks, etc. + * + * @see https://next-auth.js.org/configuration/options + */ +export const authOptions: NextAuthOptions = { + callbacks: { + session: ({ session, user }) => ({ + ...session, + user: { + ...session.user, + id: user.id, + }, + }), + }, + adapter: PrismaAdapter(db), + providers: [ + DiscordProvider({ + clientId: env.DISCORD_CLIENT_ID, + clientSecret: env.DISCORD_CLIENT_SECRET, + }), + /** + * ...add more providers here. + * + * Most other providers require a bit more work than the Discord provider. For example, the + * GitHub provider requires you to add the `refresh_token_expires_in` field to the Account + * model. Refer to the NextAuth.js docs for the provider you want to use. Example: + * + * @see https://next-auth.js.org/providers/github + */ + ], +}; + +/** + * Wrapper for `getServerSession` so that you don't need to import the `authOptions` in every file. + * + * @see https://next-auth.js.org/configuration/nextjs + */ +export const getServerAuthSession = () => getServerSession(authOptions); diff --git a/cli/template/extras/src/server/auth/base.ts b/cli/template/extras/src/server/auth-pages/base.ts similarity index 100% rename from cli/template/extras/src/server/auth/base.ts rename to cli/template/extras/src/server/auth-pages/base.ts diff --git a/cli/template/extras/src/server/auth/with-drizzle.ts b/cli/template/extras/src/server/auth-pages/with-drizzle.ts similarity index 100% rename from cli/template/extras/src/server/auth/with-drizzle.ts rename to cli/template/extras/src/server/auth-pages/with-drizzle.ts diff --git a/cli/template/extras/src/server/auth/with-prisma.ts b/cli/template/extras/src/server/auth-pages/with-prisma.ts similarity index 100% rename from cli/template/extras/src/server/auth/with-prisma.ts rename to cli/template/extras/src/server/auth-pages/with-prisma.ts diff --git a/cli/template/extras/src/server/db/drizzle-schema-auth.ts b/cli/template/extras/src/server/db/drizzle-schema-auth.ts index ee154f7e78..dd11fce21c 100644 --- a/cli/template/extras/src/server/db/drizzle-schema-auth.ts +++ b/cli/template/extras/src/server/db/drizzle-schema-auth.ts @@ -7,7 +7,6 @@ import { primaryKey, text, timestamp, - uniqueIndex, varchar, } from "drizzle-orm/mysql-core"; import { type AdapterAccount } from "next-auth/adapters"; @@ -20,18 +19,20 @@ import { type AdapterAccount } from "next-auth/adapters"; */ export const mysqlTable = mysqlTableCreator((name) => `project1_${name}`); -export const example = mysqlTable( - "example", +export const posts = mysqlTable( + "post", { id: bigint("id", { mode: "number" }).primaryKey().autoincrement(), name: varchar("name", { length: 256 }), + createdById: varchar("createdById", { length: 255 }).notNull(), createdAt: timestamp("created_at") .default(sql`CURRENT_TIMESTAMP`) .notNull(), updatedAt: timestamp("updatedAt").onUpdateNow(), }, (example) => ({ - nameIndex: uniqueIndex("name_idx").on(example.name), + createdByIdIdx: index("createdById_idx").on(example.createdById), + nameIndex: index("name_idx").on(example.name), }) ); diff --git a/cli/template/extras/src/server/db/drizzle-schema-base.ts b/cli/template/extras/src/server/db/drizzle-schema-base.ts index bf0360bc6a..7e50b8ee7d 100644 --- a/cli/template/extras/src/server/db/drizzle-schema-base.ts +++ b/cli/template/extras/src/server/db/drizzle-schema-base.ts @@ -4,9 +4,9 @@ import { sql } from "drizzle-orm"; import { bigint, + index, mysqlTableCreator, timestamp, - uniqueIndex, varchar, } from "drizzle-orm/mysql-core"; @@ -18,8 +18,8 @@ import { */ export const mysqlTable = mysqlTableCreator((name) => `project1_${name}`); -export const example = mysqlTable( - "example", +export const posts = mysqlTable( + "post", { id: bigint("id", { mode: "number" }).primaryKey().autoincrement(), name: varchar("name", { length: 256 }), @@ -29,6 +29,6 @@ export const example = mysqlTable( updatedAt: timestamp("updatedAt").onUpdateNow(), }, (example) => ({ - nameIndex: uniqueIndex("name_idx").on(example.name), + nameIndex: index("name_idx").on(example.name), }) ); diff --git a/cli/template/extras/src/trpc/react.tsx b/cli/template/extras/src/trpc/react.tsx new file mode 100644 index 0000000000..6429613d25 --- /dev/null +++ b/cli/template/extras/src/trpc/react.tsx @@ -0,0 +1,47 @@ +"use client"; + +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { loggerLink, unstable_httpBatchStreamLink } from "@trpc/client"; +import { createTRPCReact } from "@trpc/react-query"; +import { useState } from "react"; + +import { type AppRouter } from "~/server/api/root"; +import { getUrl, transformer } from "./shared"; + +export const api = createTRPCReact(); + +export function TRPCReactProvider(props: { + children: React.ReactNode; + headers: Headers; +}) { + const [queryClient] = useState(() => new QueryClient()); + + const [trpcClient] = useState(() => + api.createClient({ + transformer, + links: [ + loggerLink({ + enabled: (op) => + process.env.NODE_ENV === "development" || + (op.direction === "down" && op.result instanceof Error), + }), + unstable_httpBatchStreamLink({ + url: getUrl(), + headers() { + const heads = new Map(props.headers); + heads.set("x-trpc-source", "react"); + return Object.fromEntries(heads); + }, + }), + ], + }) + ); + + return ( + + + {props.children} + + + ); +} diff --git a/cli/template/extras/src/trpc/server.ts b/cli/template/extras/src/trpc/server.ts new file mode 100644 index 0000000000..6984f458fc --- /dev/null +++ b/cli/template/extras/src/trpc/server.ts @@ -0,0 +1,28 @@ +import { + createTRPCProxyClient, + loggerLink, + unstable_httpBatchStreamLink, +} from "@trpc/client"; +import { headers } from "next/headers"; + +import { type AppRouter } from "~/server/api/root"; +import { getUrl, transformer } from "./shared"; + +export const api = createTRPCProxyClient({ + transformer, + links: [ + loggerLink({ + enabled: (op) => + process.env.NODE_ENV === "development" || + (op.direction === "down" && op.result instanceof Error), + }), + unstable_httpBatchStreamLink({ + url: getUrl(), + headers() { + const heads = new Map(headers()); + heads.set("x-trpc-source", "rsc"); + return Object.fromEntries(heads); + }, + }), + ], +}); diff --git a/cli/template/extras/src/trpc/shared.ts b/cli/template/extras/src/trpc/shared.ts new file mode 100644 index 0000000000..46005045df --- /dev/null +++ b/cli/template/extras/src/trpc/shared.ts @@ -0,0 +1,30 @@ +import { type inferRouterInputs, type inferRouterOutputs } from "@trpc/server"; +import superjson from "superjson"; + +import { type AppRouter } from "~/server/api/root"; + +export const transformer = superjson; + +function getBaseUrl() { + if (typeof window !== "undefined") return ""; + if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`; + return `http://localhost:${process.env.PORT ?? 3000}`; +} + +export function getUrl() { + return getBaseUrl() + "/api/trpc"; +} + +/** + * Inference helper for inputs. + * + * @example type HelloInput = RouterInputs['example']['hello'] + */ +export type RouterInputs = inferRouterInputs; + +/** + * Inference helper for outputs. + * + * @example type HelloOutput = RouterOutputs['example']['hello'] + */ +export type RouterOutputs = inferRouterOutputs; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f93a29bf1a..3f85d9e3b7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -167,9 +167,12 @@ importers: superjson: specifier: ^1.12.2 version: 1.12.2 + tailwindcss: + specifier: ^3.3.2 + version: 3.3.2 tsup: specifier: ^6.7.0 - version: 6.7.0(typescript@5.0.4) + version: 6.7.0(postcss@8.4.27)(typescript@5.0.4) type-fest: specifier: ^3.7.0 version: 3.7.0 @@ -5514,6 +5517,7 @@ packages: glob-parent: 5.1.2 merge2: 1.4.1 micromatch: 4.0.5 + dev: false /fast-glob@3.3.1: resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} @@ -5833,7 +5837,7 @@ packages: dependencies: array-union: 2.1.0 dir-glob: 3.0.1 - fast-glob: 3.2.12 + fast-glob: 3.3.1 ignore: 5.2.4 merge2: 1.4.1 slash: 3.0.0 @@ -5843,7 +5847,7 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} dependencies: dir-glob: 3.0.1 - fast-glob: 3.2.12 + fast-glob: 3.3.1 ignore: 5.2.4 merge2: 1.4.1 slash: 4.0.0 @@ -7947,7 +7951,7 @@ packages: camelcase-css: 2.0.1 postcss: 8.4.27 - /postcss-load-config@3.1.4: + /postcss-load-config@3.1.4(postcss@8.4.27): resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==} engines: {node: '>= 10'} peerDependencies: @@ -7960,6 +7964,7 @@ packages: optional: true dependencies: lilconfig: 2.1.0 + postcss: 8.4.27 yaml: 1.10.2 dev: true @@ -9252,7 +9257,7 @@ packages: chokidar: 3.5.3 didyoumean: 1.2.2 dlv: 1.1.3 - fast-glob: 3.2.12 + fast-glob: 3.3.1 glob-parent: 6.0.2 is-glob: 4.0.3 jiti: 1.18.2 @@ -9477,7 +9482,7 @@ packages: /tslib@2.5.0: resolution: {integrity: sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==} - /tsup@6.7.0(typescript@5.0.4): + /tsup@6.7.0(postcss@8.4.27)(typescript@5.0.4): resolution: {integrity: sha512-L3o8hGkaHnu5TdJns+mCqFsDBo83bJ44rlK7e6VdanIvpea4ArPcU3swWGsLVbXak1PqQx/V+SSmFPujBK+zEQ==} engines: {node: '>=14.18'} hasBin: true @@ -9501,7 +9506,8 @@ packages: execa: 5.1.1 globby: 11.1.0 joycon: 3.1.1 - postcss-load-config: 3.1.4 + postcss: 8.4.27 + postcss-load-config: 3.1.4(postcss@8.4.27) resolve-from: 5.0.0 rollup: 3.21.0 source-map: 0.8.0-beta.0