From 7f441ef6705c529b9fe3a0e61ea58697b3d8649d Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Mon, 14 Apr 2025 21:45:28 +0530 Subject: [PATCH] add orpc --- .changeset/smart-days-argue.md | 5 + .vscode/settings.json | 8 + apps/cli/package.json | 141 +- apps/cli/src/constants.ts | 13 +- apps/cli/src/helpers/addons-setup.ts | 112 +- apps/cli/src/helpers/api-setup.ts | 37 + apps/cli/src/helpers/auth-setup.ts | 28 +- .../src/helpers/backend-framework-setup.ts | 11 +- apps/cli/src/helpers/create-project.ts | 114 +- apps/cli/src/helpers/db-setup.ts | 52 +- apps/cli/src/helpers/env-setup.ts | 6 +- apps/cli/src/helpers/examples-setup.ts | 390 +----- apps/cli/src/helpers/mongodb-atlas-setup.ts | 5 +- apps/cli/src/helpers/neon-setup.ts | 65 +- apps/cli/src/helpers/post-installation.ts | 101 +- apps/cli/src/helpers/prisma-postgres-setup.ts | 31 +- apps/cli/src/helpers/project-config.ts | 74 +- apps/cli/src/helpers/runtime-setup.ts | 96 +- apps/cli/src/helpers/starlight-setup.ts | 53 +- apps/cli/src/helpers/tauri-setup.ts | 57 +- apps/cli/src/helpers/template-manager.ts | 855 ++++++------ apps/cli/src/helpers/turso-setup.ts | 25 +- apps/cli/src/index.ts | 108 +- apps/cli/src/prompts/addons.ts | 5 + apps/cli/src/prompts/api.ts | 58 + apps/cli/src/prompts/auth.ts | 1 - apps/cli/src/prompts/config-prompts.ts | 21 +- apps/cli/src/prompts/install.ts | 10 +- apps/cli/src/types.ts | 31 +- apps/cli/src/utils/add-package-deps.ts | 23 +- apps/cli/src/utils/display-config.ts | 71 +- .../utils/generate-reproducible-command.ts | 6 +- .../utils/get-package-execution-command.ts | 23 + apps/cli/src/utils/template-processor.ts | 37 + .../template/base/apps/server/src/lib/trpc.ts | 8 - .../base/apps/server/src/routers/index.ts | 11 - .../base/apps/web-base/src/utils/trpc.ts | 33 - .../apps/web-next/src/components/header.tsx | 30 - .../base/apps/web-next/src/utils/trpc.ts | 33 - .../src/components/header.tsx | 31 - .../base/apps/web-react-router/vite.config.ts | 8 - .../src/components/header.tsx | 31 - .../apps/web-tanstack-router/vite.config.ts | 14 - .../src/components/header.tsx | 27 - .../apps/web-tanstack-start/src/router.tsx | 70 - .../apps/web-tanstack-start/src/utils/trpc.ts | 5 - apps/cli/template/base/package.json | 18 - .../server/src/routers/with-drizzle-todo.ts | 34 - .../server/src/lib/with-elysia-context.ts | 18 - .../server/src/lib/with-express-context.ts | 14 - .../apps/server/src/lib/with-hono-context.ts | 18 - .../apps/server/src/lib/with-next-context.ts | 14 - .../apps/server/src/routers/index.ts | 17 - .../server/src/with-drizzle-mysql-lib/auth.ts | 15 - .../src/with-drizzle-postgres-lib/auth.ts | 15 - .../src/with-drizzle-sqlite-lib/auth.ts | 15 - .../apps/server/src/with-express-index.ts | 34 - .../apps/server/src/with-hono-index.ts | 39 - .../src/with-prisma-mongodb-lib/auth.ts | 11 - .../server/src/with-prisma-mysql-lib/auth.ts | 11 - .../src/with-prisma-postgres-lib/auth.ts | 11 - .../server/src/with-prisma-sqlite-lib/auth.ts | 11 - .../apps/web-base/src/lib/auth-client.ts | 5 - .../apps/web-next/src/components/header.tsx | 33 - .../apps/web-next/src/lib/auth-client.ts | 5 - .../with-auth/apps/web-next/src/utils/trpc.ts | 39 - .../src/components/header.tsx | 34 - .../apps/web-react-router/src/utils/trpc.ts | 39 - .../src/components/header.tsx | 34 - .../web-tanstack-router/src/utils/trpc.ts | 39 - .../src/components/header.tsx | 32 - .../with-elysia/apps/server/src/index.ts | 27 - .../apps/server/src/lib/context.ts | 13 - .../with-express/apps/server/src/index.ts | 29 - .../apps/server/src/lib/context.ts | 9 - .../with-hono/apps/server/src/index.ts | 33 - .../with-hono/apps/server/src/lib/context.ts | 13 - .../with-next/apps/server/src/lib/context.ts | 9 - .../addons/biome}/biome.json | 0 .../addons/husky}/.husky/pre-commit | 0 .../addons/pwa}/apps/web/public/logo.png | Bin .../addons/pwa}/apps/web/pwa-assets.config.ts | 0 .../addons/turborepo}/turbo.json | 0 .../orpc/server/base/src/lib/context.ts.hbs | 105 ++ .../api/orpc/server/base/src/lib/orpc.ts.hbs | 17 + .../next/src/app/rpc/[...all]/route.ts.hbs | 23 + .../api/orpc/web/base/src/utils/orpc.ts.hbs | 57 + .../trpc/server/base/src/lib/context.ts.hbs | 108 ++ .../api/trpc/server/base/src/lib/trpc.ts.hbs} | 2 + .../server/next}/src/app/trpc/[trpc]/route.ts | 0 .../api/trpc/web/base/src/utils/trpc.ts.hbs | 100 ++ .../auth}/native/app/(drawer)/index.tsx | 0 .../auth}/native/components/sign-in.tsx | 0 .../auth}/native/components/sign-up.tsx | 0 .../auth}/native/lib/auth-client.ts | 0 .../auth}/native/utils/trpc.ts | 0 .../auth/server/base/src/lib/auth.ts.hbs | 30 + .../db/drizzle/mysql}/src/db/schema/auth.ts | 0 .../drizzle/postgres}/src/db/schema/auth.ts | 0 .../db/drizzle/sqlite}/src/db/schema/auth.ts | 0 .../prisma/mongodb}/prisma/schema/auth.prisma | 0 .../prisma/mysql}/prisma/schema/auth.prisma | 0 .../postgres}/prisma/schema/auth.prisma | 0 .../prisma/sqlite}/prisma/schema/auth.prisma | 0 .../next/src/app}/api/auth/[...all]/route.ts | 0 .../auth/web/base/src/lib/auth-client.ts.hbs | 10 + .../web/next/src/app/dashboard/page.tsx.hbs} | 10 + .../auth/web/next}/src/app/login/page.tsx | 0 .../web/next}/src/components/sign-in-form.tsx | 0 .../web/next}/src/components/sign-up-form.tsx | 0 .../next}/src/components/theme-provider.tsx | 0 .../web/next}/src/components/user-menu.tsx | 0 .../src/components/sign-in-form.tsx | 0 .../src/components/sign-up-form.tsx | 0 .../src/components/user-menu.tsx | 0 .../src/routes/dashboard.tsx.hbs} | 10 + .../web/react-router}/src/routes/login.tsx | 0 .../src/components/sign-in-form.tsx | 0 .../src/components/sign-up-form.tsx | 0 .../src/components/user-menu.tsx | 0 .../src/routes/dashboard.tsx.hbs} | 10 + .../web/tanstack-router}/src/routes/login.tsx | 0 .../src/components/sign-in-form.tsx | 0 .../src/components/sign-up-form.tsx | 0 .../src/components/user-menu.tsx | 0 .../src/routes/dashboard.tsx.hbs} | 15 + .../web/tanstack-start}/src/routes/login.tsx | 0 .../backend/elysia/src/index.ts.hbs} | 41 +- .../backend/express/src/index.ts.hbs | 78 ++ .../templates/backend/hono/src/index.ts.hbs | 105 ++ .../backend/next}/next-env.d.ts | 0 .../backend/next}/next.config.ts | 0 .../backend/next}/package.json | 2 - .../backend/next}/src/app/route.ts | 0 .../backend/next}/src/middleware.ts | 0 .../backend/next}/tsconfig.json | 0 .../backend/server-base}/_gitignore | 0 .../backend/server-base}/package.json | 1 - .../server-base/src/routers/index.ts.hbs | 51 + .../backend/server-base}/tsconfig.json | 0 .../{template => templates}/base/_gitignore | 0 apps/cli/templates/base/package.json | 10 + .../db/drizzle/mysql}/drizzle.config.ts | 0 .../db/drizzle/mysql}/src/db/index.ts | 0 .../db/drizzle/postgres}/drizzle.config.ts | 0 .../db/drizzle/postgres}/src/db/index.ts | 0 .../db/drizzle/sqlite}/drizzle.config.ts | 0 .../db/drizzle/sqlite}/src/db/index.ts | 0 .../db/prisma/mongodb}/prisma/index.ts | 0 .../mongodb}/prisma/schema/schema.prisma | 0 .../db/prisma/mysql}/prisma/index.ts | 0 .../prisma/mysql}/prisma/schema/schema.prisma | 0 .../db/prisma/postgres}/prisma/index.ts | 0 .../postgres}/prisma/schema/schema.prisma | 0 .../db/prisma/sqlite}/prisma/index.ts | 0 .../sqlite}/prisma/schema/schema.prisma | 0 .../ai/apps/react-router}/src/routes/ai.tsx | 0 .../apps/tanstack-router}/src/routes/ai.tsx | 0 .../ai/apps/tanstack-start}/src/routes/ai.tsx | 0 .../drizzle/base/src/routers/todo.ts.hbs | 79 ++ .../drizzle/mysql}/src/db/schema/todo.ts | 0 .../drizzle/postgres}/src/db/schema/todo.ts | 0 .../drizzle/sqlite}/src/db/schema/todo.ts | 4 +- .../prisma/base/trpc/src/routers/todo.ts} | 0 .../prisma/mongodb}/prisma/schema/todo.prisma | 0 .../prisma/mysql}/prisma/schema/todo.prisma | 0 .../postgres}/prisma/schema/todo.prisma | 0 .../prisma/sqlite}/prisma/schema/todo.prisma | 0 .../react-router/src/routes/todos.tsx.hbs} | 28 + .../tanstack-router/src/routes/todos.tsx.hbs} | 28 + .../tanstack-start/src/routes/todos.tsx.hbs} | 33 + apps/cli/templates/extras/.npmrc | 1 + .../extras}/pnpm-workspace.yaml | 0 .../frontend}/native/_gitignore | 0 .../frontend}/native/app-env.d.ts | 0 .../frontend}/native/app.json | 0 .../native/app/(drawer)/(tabs)/_layout.tsx | 0 .../native/app/(drawer)/(tabs)/index.tsx | 0 .../native/app/(drawer)/(tabs)/two.tsx | 0 .../frontend}/native/app/(drawer)/_layout.tsx | 0 .../frontend}/native/app/(drawer)/index.tsx | 0 .../frontend}/native/app/+html.tsx | 0 .../frontend}/native/app/+not-found.tsx | 0 .../frontend}/native/app/_layout.tsx | 0 .../frontend}/native/app/modal.tsx | 0 .../frontend}/native/assets/adaptive-icon.png | Bin .../frontend}/native/assets/favicon.png | Bin .../frontend}/native/assets/icon.png | Bin .../frontend}/native/assets/splash.png | Bin .../frontend}/native/babel.config.js | 0 .../frontend}/native/components/container.tsx | 0 .../native/components/header-button.tsx | 0 .../native/components/tabbar-icon.tsx | 0 .../frontend}/native/global.css | 0 .../native/lib/android-navigation-bar.tsx | 0 .../frontend}/native/lib/constants.ts | 0 .../frontend}/native/lib/use-color-scheme.ts | 0 .../frontend}/native/metro.config.js | 0 .../frontend}/native/package.json | 0 .../frontend}/native/tailwind.config.js | 0 .../frontend}/native/tsconfig.json | 0 .../frontend}/native/utils/trpc.ts | 0 .../frontend/next}/next-env.d.ts | 0 .../frontend/next}/next.config.ts | 0 .../frontend/next}/package.json | 2 - .../frontend/next}/postcss.config.mjs | 0 .../frontend/next}/src/app/favicon.ico | Bin .../frontend/next}/src/app/layout.tsx | 0 .../frontend/next/src/app/page.tsx.hbs} | 10 + .../next}/src/components/mode-toggle.tsx | 0 .../next/src/components/providers.tsx.hbs} | 12 + .../next}/src/components/theme-provider.tsx | 0 .../frontend/next}/tsconfig.json | 0 .../frontend/react-router}/package.json | 3 - .../frontend/react-router}/public/favicon.ico | Bin .../react-router}/react-router.config.ts | 0 .../src/components/mode-toggle.tsx | 0 .../src/components/theme-provider.tsx | 0 .../frontend/react-router/src/root.tsx.hbs} | 29 +- .../frontend/react-router}/src/routes.ts | 0 .../react-router/src/routes/_index.tsx.hbs} | 11 + .../frontend/react-router}/tsconfig.json | 0 .../frontend/react-router/vite.config.ts.hbs | 37 + .../frontend/tanstack-router}/index.html | 0 .../frontend/tanstack-router}/package.json | 4 - .../src/components/mode-toggle.tsx | 0 .../src/components/theme-provider.tsx | 0 .../tanstack-router/src/main.tsx.hbs} | 20 + .../src/routes/__root.tsx.hbs} | 48 + .../tanstack-router/src/routes/index.tsx.hbs} | 10 + .../frontend/tanstack-router}/tsconfig.json | 0 .../tanstack-router/vite.config.ts.hbs | 43 + .../frontend/tanstack-start}/app.config.ts | 0 .../frontend/tanstack-start}/package.json | 3 - .../tanstack-start}/public/robots.txt | 0 .../frontend/tanstack-start}/src/api.ts | 0 .../frontend/tanstack-start}/src/client.tsx | 0 .../tanstack-start/src/router.tsx.hbs} | 32 +- .../tanstack-start/src/routes/__root.tsx.hbs} | 13 + .../tanstack-start/src/routes/index.tsx.hbs} | 11 + .../frontend/tanstack-start}/src/ssr.tsx | 0 .../frontend/tanstack-start}/tsconfig.json | 0 .../frontend}/web-base/_gitignore | 0 .../frontend}/web-base/components.json | 0 .../web-base/src/components/header.tsx.hbs | 80 ++ .../web-base/src/components/loader.tsx | 0 .../web-base/src/components/ui/button.tsx | 0 .../web-base/src/components/ui/card.tsx | 0 .../web-base/src/components/ui/checkbox.tsx | 0 .../src/components/ui/dropdown-menu.tsx | 0 .../web-base/src/components/ui/input.tsx | 0 .../web-base/src/components/ui/label.tsx | 0 .../web-base/src/components/ui/skeleton.tsx | 0 .../web-base/src/components/ui/sonner.tsx | 0 .../frontend}/web-base/src/index.css | 0 .../frontend}/web-base/src/lib/utils.ts | 0 apps/web/package.json | 2 +- .../app/(home)/_components/CodeContainer.tsx | 2 +- .../_components/CustomizableSection.tsx | 2 +- .../app/(home)/_components/StackArchitech.tsx | 1192 ++++++++++------- .../app/(home)/_components/TechShowcase.tsx | 2 +- .../app/(home)/_components/Testimonials.tsx | 2 +- apps/web/src/app/(home)/layout.tsx | 6 +- apps/web/src/app/(home)/new/page.tsx | 53 +- apps/web/src/app/(home)/page.tsx | 206 ++- apps/web/src/lib/constant.ts | 40 +- biome.json | 4 +- bun.lock | 53 +- 268 files changed, 3513 insertions(+), 3039 deletions(-) create mode 100644 .changeset/smart-days-argue.md create mode 100644 .vscode/settings.json create mode 100644 apps/cli/src/helpers/api-setup.ts create mode 100644 apps/cli/src/prompts/api.ts create mode 100644 apps/cli/src/utils/get-package-execution-command.ts create mode 100644 apps/cli/src/utils/template-processor.ts delete mode 100644 apps/cli/template/base/apps/server/src/lib/trpc.ts delete mode 100644 apps/cli/template/base/apps/server/src/routers/index.ts delete mode 100644 apps/cli/template/base/apps/web-base/src/utils/trpc.ts delete mode 100644 apps/cli/template/base/apps/web-next/src/components/header.tsx delete mode 100644 apps/cli/template/base/apps/web-next/src/utils/trpc.ts delete mode 100644 apps/cli/template/base/apps/web-react-router/src/components/header.tsx delete mode 100644 apps/cli/template/base/apps/web-react-router/vite.config.ts delete mode 100644 apps/cli/template/base/apps/web-tanstack-router/src/components/header.tsx delete mode 100644 apps/cli/template/base/apps/web-tanstack-router/vite.config.ts delete mode 100644 apps/cli/template/base/apps/web-tanstack-start/src/components/header.tsx delete mode 100644 apps/cli/template/base/apps/web-tanstack-start/src/router.tsx delete mode 100644 apps/cli/template/base/apps/web-tanstack-start/src/utils/trpc.ts delete mode 100644 apps/cli/template/base/package.json delete mode 100644 apps/cli/template/examples/todo/apps/server/src/routers/with-drizzle-todo.ts delete mode 100644 apps/cli/template/with-auth/apps/server/src/lib/with-elysia-context.ts delete mode 100644 apps/cli/template/with-auth/apps/server/src/lib/with-express-context.ts delete mode 100644 apps/cli/template/with-auth/apps/server/src/lib/with-hono-context.ts delete mode 100644 apps/cli/template/with-auth/apps/server/src/lib/with-next-context.ts delete mode 100644 apps/cli/template/with-auth/apps/server/src/routers/index.ts delete mode 100644 apps/cli/template/with-auth/apps/server/src/with-drizzle-mysql-lib/auth.ts delete mode 100644 apps/cli/template/with-auth/apps/server/src/with-drizzle-postgres-lib/auth.ts delete mode 100644 apps/cli/template/with-auth/apps/server/src/with-drizzle-sqlite-lib/auth.ts delete mode 100644 apps/cli/template/with-auth/apps/server/src/with-express-index.ts delete mode 100644 apps/cli/template/with-auth/apps/server/src/with-hono-index.ts delete mode 100644 apps/cli/template/with-auth/apps/server/src/with-prisma-mongodb-lib/auth.ts delete mode 100644 apps/cli/template/with-auth/apps/server/src/with-prisma-mysql-lib/auth.ts delete mode 100644 apps/cli/template/with-auth/apps/server/src/with-prisma-postgres-lib/auth.ts delete mode 100644 apps/cli/template/with-auth/apps/server/src/with-prisma-sqlite-lib/auth.ts delete mode 100644 apps/cli/template/with-auth/apps/web-base/src/lib/auth-client.ts delete mode 100644 apps/cli/template/with-auth/apps/web-next/src/components/header.tsx delete mode 100644 apps/cli/template/with-auth/apps/web-next/src/lib/auth-client.ts delete mode 100644 apps/cli/template/with-auth/apps/web-next/src/utils/trpc.ts delete mode 100644 apps/cli/template/with-auth/apps/web-react-router/src/components/header.tsx delete mode 100644 apps/cli/template/with-auth/apps/web-react-router/src/utils/trpc.ts delete mode 100644 apps/cli/template/with-auth/apps/web-tanstack-router/src/components/header.tsx delete mode 100644 apps/cli/template/with-auth/apps/web-tanstack-router/src/utils/trpc.ts delete mode 100644 apps/cli/template/with-auth/apps/web-tanstack-start/src/components/header.tsx delete mode 100644 apps/cli/template/with-elysia/apps/server/src/index.ts delete mode 100644 apps/cli/template/with-elysia/apps/server/src/lib/context.ts delete mode 100644 apps/cli/template/with-express/apps/server/src/index.ts delete mode 100644 apps/cli/template/with-express/apps/server/src/lib/context.ts delete mode 100644 apps/cli/template/with-hono/apps/server/src/index.ts delete mode 100644 apps/cli/template/with-hono/apps/server/src/lib/context.ts delete mode 100644 apps/cli/template/with-next/apps/server/src/lib/context.ts rename apps/cli/{template/with-biome => templates/addons/biome}/biome.json (100%) rename apps/cli/{template/with-husky => templates/addons/husky}/.husky/pre-commit (100%) rename apps/cli/{template/with-pwa => templates/addons/pwa}/apps/web/public/logo.png (100%) rename apps/cli/{template/with-pwa => templates/addons/pwa}/apps/web/pwa-assets.config.ts (100%) rename apps/cli/{template/base => templates/addons/turborepo}/turbo.json (100%) create mode 100644 apps/cli/templates/api/orpc/server/base/src/lib/context.ts.hbs create mode 100644 apps/cli/templates/api/orpc/server/base/src/lib/orpc.ts.hbs create mode 100644 apps/cli/templates/api/orpc/server/next/src/app/rpc/[...all]/route.ts.hbs create mode 100644 apps/cli/templates/api/orpc/web/base/src/utils/orpc.ts.hbs create mode 100644 apps/cli/templates/api/trpc/server/base/src/lib/context.ts.hbs rename apps/cli/{template/with-auth/apps/server/src/lib/trpc.ts => templates/api/trpc/server/base/src/lib/trpc.ts.hbs} (96%) rename apps/cli/{template/with-next/apps/server => templates/api/trpc/server/next}/src/app/trpc/[trpc]/route.ts (100%) create mode 100644 apps/cli/templates/api/trpc/web/base/src/utils/trpc.ts.hbs rename apps/cli/{template/with-auth/apps => templates/auth}/native/app/(drawer)/index.tsx (100%) rename apps/cli/{template/with-auth/apps => templates/auth}/native/components/sign-in.tsx (100%) rename apps/cli/{template/with-auth/apps => templates/auth}/native/components/sign-up.tsx (100%) rename apps/cli/{template/with-auth/apps => templates/auth}/native/lib/auth-client.ts (100%) rename apps/cli/{template/with-auth/apps => templates/auth}/native/utils/trpc.ts (100%) create mode 100644 apps/cli/templates/auth/server/base/src/lib/auth.ts.hbs rename apps/cli/{template/with-drizzle-mysql/apps/server => templates/auth/server/db/drizzle/mysql}/src/db/schema/auth.ts (100%) rename apps/cli/{template/with-drizzle-postgres/apps/server => templates/auth/server/db/drizzle/postgres}/src/db/schema/auth.ts (100%) rename apps/cli/{template/with-drizzle-sqlite/apps/server => templates/auth/server/db/drizzle/sqlite}/src/db/schema/auth.ts (100%) rename apps/cli/{template/with-prisma-sqlite/apps/server => templates/auth/server/db/prisma/mongodb}/prisma/schema/auth.prisma (100%) rename apps/cli/{template/with-prisma-mysql/apps/server => templates/auth/server/db/prisma/mysql}/prisma/schema/auth.prisma (100%) rename apps/cli/{template/with-prisma-postgres/apps/server => templates/auth/server/db/prisma/postgres}/prisma/schema/auth.prisma (100%) rename apps/cli/{template/with-prisma-mongodb/apps/server => templates/auth/server/db/prisma/sqlite}/prisma/schema/auth.prisma (100%) rename apps/cli/{template/with-auth/apps/server/src/with-next-app => templates/auth/server/next/src/app}/api/auth/[...all]/route.ts (100%) create mode 100644 apps/cli/templates/auth/web/base/src/lib/auth-client.ts.hbs rename apps/cli/{template/with-auth/apps/web-next/src/app/dashboard/page.tsx => templates/auth/web/next/src/app/dashboard/page.tsx.hbs} (76%) rename apps/cli/{template/with-auth/apps/web-next => templates/auth/web/next}/src/app/login/page.tsx (100%) rename apps/cli/{template/with-auth/apps/web-next => templates/auth/web/next}/src/components/sign-in-form.tsx (100%) rename apps/cli/{template/with-auth/apps/web-next => templates/auth/web/next}/src/components/sign-up-form.tsx (100%) rename apps/cli/{template/with-auth/apps/web-next => templates/auth/web/next}/src/components/theme-provider.tsx (100%) rename apps/cli/{template/with-auth/apps/web-next => templates/auth/web/next}/src/components/user-menu.tsx (100%) rename apps/cli/{template/with-auth/apps/web-react-router => templates/auth/web/react-router}/src/components/sign-in-form.tsx (100%) rename apps/cli/{template/with-auth/apps/web-react-router => templates/auth/web/react-router}/src/components/sign-up-form.tsx (100%) rename apps/cli/{template/with-auth/apps/web-react-router => templates/auth/web/react-router}/src/components/user-menu.tsx (100%) rename apps/cli/{template/with-auth/apps/web-react-router/src/routes/dashboard.tsx => templates/auth/web/react-router/src/routes/dashboard.tsx.hbs} (75%) rename apps/cli/{template/with-auth/apps/web-react-router => templates/auth/web/react-router}/src/routes/login.tsx (100%) rename apps/cli/{template/with-auth/apps/web-tanstack-start => templates/auth/web/tanstack-router}/src/components/sign-in-form.tsx (100%) rename apps/cli/{template/with-auth/apps/web-tanstack-start => templates/auth/web/tanstack-router}/src/components/sign-up-form.tsx (100%) rename apps/cli/{template/with-auth/apps/web-tanstack-start => templates/auth/web/tanstack-router}/src/components/user-menu.tsx (100%) rename apps/cli/{template/with-auth/apps/web-tanstack-router/src/routes/dashboard.tsx => templates/auth/web/tanstack-router/src/routes/dashboard.tsx.hbs} (78%) rename apps/cli/{template/with-auth/apps/web-tanstack-start => templates/auth/web/tanstack-router}/src/routes/login.tsx (100%) rename apps/cli/{template/with-auth/apps/web-tanstack-router => templates/auth/web/tanstack-start}/src/components/sign-in-form.tsx (100%) rename apps/cli/{template/with-auth/apps/web-tanstack-router => templates/auth/web/tanstack-start}/src/components/sign-up-form.tsx (100%) rename apps/cli/{template/with-auth/apps/web-tanstack-router => templates/auth/web/tanstack-start}/src/components/user-menu.tsx (100%) rename apps/cli/{template/with-auth/apps/web-tanstack-start/src/routes/dashboard.tsx => templates/auth/web/tanstack-start/src/routes/dashboard.tsx.hbs} (72%) rename apps/cli/{template/with-auth/apps/web-tanstack-router => templates/auth/web/tanstack-start}/src/routes/login.tsx (100%) rename apps/cli/{template/with-auth/apps/server/src/with-elysia-index.ts => templates/backend/elysia/src/index.ts.hbs} (52%) create mode 100644 apps/cli/templates/backend/express/src/index.ts.hbs create mode 100644 apps/cli/templates/backend/hono/src/index.ts.hbs rename apps/cli/{template/with-next/apps/server => templates/backend/next}/next-env.d.ts (100%) rename apps/cli/{template/with-next/apps/server => templates/backend/next}/next.config.ts (100%) rename apps/cli/{template/with-next/apps/server => templates/backend/next}/package.json (83%) rename apps/cli/{template/with-next/apps/server => templates/backend/next}/src/app/route.ts (100%) rename apps/cli/{template/with-next/apps/server => templates/backend/next}/src/middleware.ts (100%) rename apps/cli/{template/with-next/apps/server => templates/backend/next}/tsconfig.json (100%) rename apps/cli/{template/base/apps/server => templates/backend/server-base}/_gitignore (100%) rename apps/cli/{template/base/apps/server => templates/backend/server-base}/package.json (93%) create mode 100644 apps/cli/templates/backend/server-base/src/routers/index.ts.hbs rename apps/cli/{template/base/apps/server => templates/backend/server-base}/tsconfig.json (100%) rename apps/cli/{template => templates}/base/_gitignore (100%) create mode 100644 apps/cli/templates/base/package.json rename apps/cli/{template/with-drizzle-mysql/apps/server => templates/db/drizzle/mysql}/drizzle.config.ts (100%) rename apps/cli/{template/with-drizzle-mysql/apps/server => templates/db/drizzle/mysql}/src/db/index.ts (100%) rename apps/cli/{template/with-drizzle-postgres/apps/server => templates/db/drizzle/postgres}/drizzle.config.ts (100%) rename apps/cli/{template/with-drizzle-postgres/apps/server => templates/db/drizzle/postgres}/src/db/index.ts (100%) rename apps/cli/{template/with-drizzle-sqlite/apps/server => templates/db/drizzle/sqlite}/drizzle.config.ts (100%) rename apps/cli/{template/with-drizzle-sqlite/apps/server => templates/db/drizzle/sqlite}/src/db/index.ts (100%) rename apps/cli/{template/with-prisma-sqlite/apps/server => templates/db/prisma/mongodb}/prisma/index.ts (100%) rename apps/cli/{template/with-prisma-mongodb/apps/server => templates/db/prisma/mongodb}/prisma/schema/schema.prisma (100%) rename apps/cli/{template/with-prisma-postgres/apps/server => templates/db/prisma/mysql}/prisma/index.ts (100%) rename apps/cli/{template/with-prisma-mysql/apps/server => templates/db/prisma/mysql}/prisma/schema/schema.prisma (100%) rename apps/cli/{template/with-prisma-mysql/apps/server => templates/db/prisma/postgres}/prisma/index.ts (100%) rename apps/cli/{template/with-prisma-postgres/apps/server => templates/db/prisma/postgres}/prisma/schema/schema.prisma (100%) rename apps/cli/{template/with-prisma-mongodb/apps/server => templates/db/prisma/sqlite}/prisma/index.ts (100%) rename apps/cli/{template/with-prisma-sqlite/apps/server => templates/db/prisma/sqlite}/prisma/schema/schema.prisma (100%) rename apps/cli/{template/examples/ai/apps/web-react-router => templates/examples/ai/apps/react-router}/src/routes/ai.tsx (100%) rename apps/cli/{template/examples/ai/apps/web-tanstack-start => templates/examples/ai/apps/tanstack-router}/src/routes/ai.tsx (100%) rename apps/cli/{template/examples/ai/apps/web-tanstack-router => templates/examples/ai/apps/tanstack-start}/src/routes/ai.tsx (100%) create mode 100644 apps/cli/templates/examples/todo/server/drizzle/base/src/routers/todo.ts.hbs rename apps/cli/{template/with-drizzle-mysql/apps/server => templates/examples/todo/server/drizzle/mysql}/src/db/schema/todo.ts (100%) rename apps/cli/{template/with-drizzle-postgres/apps/server => templates/examples/todo/server/drizzle/postgres}/src/db/schema/todo.ts (100%) rename apps/cli/{template/with-drizzle-sqlite/apps/server => templates/examples/todo/server/drizzle/sqlite}/src/db/schema/todo.ts (71%) rename apps/cli/{template/examples/todo/apps/server/src/routers/with-prisma-todo.ts => templates/examples/todo/server/prisma/base/trpc/src/routers/todo.ts} (100%) rename apps/cli/{template/with-prisma-mongodb/apps/server => templates/examples/todo/server/prisma/mongodb}/prisma/schema/todo.prisma (100%) rename apps/cli/{template/with-prisma-sqlite/apps/server => templates/examples/todo/server/prisma/mysql}/prisma/schema/todo.prisma (100%) rename apps/cli/{template/with-prisma-postgres/apps/server => templates/examples/todo/server/prisma/postgres}/prisma/schema/todo.prisma (100%) rename apps/cli/{template/with-prisma-mysql/apps/server => templates/examples/todo/server/prisma/sqlite}/prisma/schema/todo.prisma (100%) rename apps/cli/{template/examples/todo/apps/web-react-router/src/routes/todos.tsx => templates/examples/todo/web/react-router/src/routes/todos.tsx.hbs} (85%) rename apps/cli/{template/examples/todo/apps/web-tanstack-router/src/routes/todos.tsx => templates/examples/todo/web/tanstack-router/src/routes/todos.tsx.hbs} (85%) rename apps/cli/{template/examples/todo/apps/web-tanstack-start/src/routes/todos.tsx => templates/examples/todo/web/tanstack-start/src/routes/todos.tsx.hbs} (84%) create mode 100644 apps/cli/templates/extras/.npmrc rename apps/cli/{template/with-pnpm => templates/extras}/pnpm-workspace.yaml (100%) rename apps/cli/{template/base/apps => templates/frontend}/native/_gitignore (100%) rename apps/cli/{template/base/apps => templates/frontend}/native/app-env.d.ts (100%) rename apps/cli/{template/base/apps => templates/frontend}/native/app.json (100%) rename apps/cli/{template/base/apps => templates/frontend}/native/app/(drawer)/(tabs)/_layout.tsx (100%) rename apps/cli/{template/base/apps => templates/frontend}/native/app/(drawer)/(tabs)/index.tsx (100%) rename apps/cli/{template/base/apps => templates/frontend}/native/app/(drawer)/(tabs)/two.tsx (100%) rename apps/cli/{template/base/apps => templates/frontend}/native/app/(drawer)/_layout.tsx (100%) rename apps/cli/{template/base/apps => templates/frontend}/native/app/(drawer)/index.tsx (100%) rename apps/cli/{template/base/apps => templates/frontend}/native/app/+html.tsx (100%) rename apps/cli/{template/base/apps => templates/frontend}/native/app/+not-found.tsx (100%) rename apps/cli/{template/base/apps => templates/frontend}/native/app/_layout.tsx (100%) rename apps/cli/{template/base/apps => templates/frontend}/native/app/modal.tsx (100%) rename apps/cli/{template/base/apps => templates/frontend}/native/assets/adaptive-icon.png (100%) rename apps/cli/{template/base/apps => templates/frontend}/native/assets/favicon.png (100%) rename apps/cli/{template/base/apps => templates/frontend}/native/assets/icon.png (100%) rename apps/cli/{template/base/apps => templates/frontend}/native/assets/splash.png (100%) rename apps/cli/{template/base/apps => templates/frontend}/native/babel.config.js (100%) rename apps/cli/{template/base/apps => templates/frontend}/native/components/container.tsx (100%) rename apps/cli/{template/base/apps => templates/frontend}/native/components/header-button.tsx (100%) rename apps/cli/{template/base/apps => templates/frontend}/native/components/tabbar-icon.tsx (100%) rename apps/cli/{template/base/apps => templates/frontend}/native/global.css (100%) rename apps/cli/{template/base/apps => templates/frontend}/native/lib/android-navigation-bar.tsx (100%) rename apps/cli/{template/base/apps => templates/frontend}/native/lib/constants.ts (100%) rename apps/cli/{template/base/apps => templates/frontend}/native/lib/use-color-scheme.ts (100%) rename apps/cli/{template/base/apps => templates/frontend}/native/metro.config.js (100%) rename apps/cli/{template/base/apps => templates/frontend}/native/package.json (100%) rename apps/cli/{template/base/apps => templates/frontend}/native/tailwind.config.js (100%) rename apps/cli/{template/base/apps => templates/frontend}/native/tsconfig.json (100%) rename apps/cli/{template/base/apps => templates/frontend}/native/utils/trpc.ts (100%) rename apps/cli/{template/base/apps/web-next => templates/frontend/next}/next-env.d.ts (100%) rename apps/cli/{template/base/apps/web-next => templates/frontend/next}/next.config.ts (100%) rename apps/cli/{template/base/apps/web-next => templates/frontend/next}/package.json (93%) rename apps/cli/{template/base/apps/web-next => templates/frontend/next}/postcss.config.mjs (100%) rename apps/cli/{template/base/apps/web-next => templates/frontend/next}/src/app/favicon.ico (100%) rename apps/cli/{template/base/apps/web-next => templates/frontend/next}/src/app/layout.tsx (100%) rename apps/cli/{template/base/apps/web-next/src/app/page.tsx => templates/frontend/next/src/app/page.tsx.hbs} (93%) rename apps/cli/{template/base/apps/web-next => templates/frontend/next}/src/components/mode-toggle.tsx (100%) rename apps/cli/{template/base/apps/web-next/src/components/providers.tsx => templates/frontend/next/src/components/providers.tsx.hbs} (64%) rename apps/cli/{template/base/apps/web-next => templates/frontend/next}/src/components/theme-provider.tsx (100%) rename apps/cli/{template/base/apps/web-next => templates/frontend/next}/tsconfig.json (100%) rename apps/cli/{template/base/apps/web-react-router => templates/frontend/react-router}/package.json (92%) rename apps/cli/{template/base/apps/web-react-router => templates/frontend/react-router}/public/favicon.ico (100%) rename apps/cli/{template/base/apps/web-react-router => templates/frontend/react-router}/react-router.config.ts (100%) rename apps/cli/{template/base/apps/web-tanstack-router => templates/frontend/react-router}/src/components/mode-toggle.tsx (100%) rename apps/cli/{template/base/apps/web-tanstack-router => templates/frontend/react-router}/src/components/theme-provider.tsx (100%) rename apps/cli/{template/base/apps/web-react-router/src/root.tsx => templates/frontend/react-router/src/root.tsx.hbs} (77%) rename apps/cli/{template/base/apps/web-react-router => templates/frontend/react-router}/src/routes.ts (100%) rename apps/cli/{template/base/apps/web-react-router/src/routes/_index.tsx => templates/frontend/react-router/src/routes/_index.tsx.hbs} (94%) rename apps/cli/{template/base/apps/web-react-router => templates/frontend/react-router}/tsconfig.json (100%) create mode 100644 apps/cli/templates/frontend/react-router/vite.config.ts.hbs rename apps/cli/{template/base/apps/web-tanstack-router => templates/frontend/tanstack-router}/index.html (100%) rename apps/cli/{template/base/apps/web-tanstack-router => templates/frontend/tanstack-router}/package.json (89%) rename apps/cli/{template/base/apps/web-react-router => templates/frontend/tanstack-router}/src/components/mode-toggle.tsx (100%) rename apps/cli/{template/base/apps/web-react-router => templates/frontend/tanstack-router}/src/components/theme-provider.tsx (100%) rename apps/cli/{template/base/apps/web-tanstack-router/src/main.tsx => templates/frontend/tanstack-router/src/main.tsx.hbs} (67%) rename apps/cli/{template/base/apps/web-tanstack-router/src/routes/__root.tsx => templates/frontend/tanstack-router/src/routes/__root.tsx.hbs} (51%) rename apps/cli/{template/base/apps/web-tanstack-router/src/routes/index.tsx => templates/frontend/tanstack-router/src/routes/index.tsx.hbs} (93%) rename apps/cli/{template/base/apps/web-tanstack-router => templates/frontend/tanstack-router}/tsconfig.json (100%) create mode 100644 apps/cli/templates/frontend/tanstack-router/vite.config.ts.hbs rename apps/cli/{template/base/apps/web-tanstack-start => templates/frontend/tanstack-start}/app.config.ts (100%) rename apps/cli/{template/base/apps/web-tanstack-start => templates/frontend/tanstack-start}/package.json (93%) rename apps/cli/{template/base/apps/web-tanstack-start => templates/frontend/tanstack-start}/public/robots.txt (100%) rename apps/cli/{template/base/apps/web-tanstack-start => templates/frontend/tanstack-start}/src/api.ts (100%) rename apps/cli/{template/base/apps/web-tanstack-start => templates/frontend/tanstack-start}/src/client.tsx (100%) rename apps/cli/{template/with-auth/apps/web-tanstack-start/src/router.tsx => templates/frontend/tanstack-start/src/router.tsx.hbs} (76%) rename apps/cli/{template/base/apps/web-tanstack-start/src/routes/__root.tsx => templates/frontend/tanstack-start/src/routes/__root.tsx.hbs} (87%) rename apps/cli/{template/base/apps/web-tanstack-start/src/routes/index.tsx => templates/frontend/tanstack-start/src/routes/index.tsx.hbs} (93%) rename apps/cli/{template/base/apps/web-tanstack-start => templates/frontend/tanstack-start}/src/ssr.tsx (100%) rename apps/cli/{template/base/apps/web-tanstack-start => templates/frontend/tanstack-start}/tsconfig.json (100%) rename apps/cli/{template/base/apps => templates/frontend}/web-base/_gitignore (100%) rename apps/cli/{template/base/apps => templates/frontend}/web-base/components.json (100%) create mode 100644 apps/cli/templates/frontend/web-base/src/components/header.tsx.hbs rename apps/cli/{template/base/apps => templates/frontend}/web-base/src/components/loader.tsx (100%) rename apps/cli/{template/base/apps => templates/frontend}/web-base/src/components/ui/button.tsx (100%) rename apps/cli/{template/base/apps => templates/frontend}/web-base/src/components/ui/card.tsx (100%) rename apps/cli/{template/base/apps => templates/frontend}/web-base/src/components/ui/checkbox.tsx (100%) rename apps/cli/{template/base/apps => templates/frontend}/web-base/src/components/ui/dropdown-menu.tsx (100%) rename apps/cli/{template/base/apps => templates/frontend}/web-base/src/components/ui/input.tsx (100%) rename apps/cli/{template/base/apps => templates/frontend}/web-base/src/components/ui/label.tsx (100%) rename apps/cli/{template/base/apps => templates/frontend}/web-base/src/components/ui/skeleton.tsx (100%) rename apps/cli/{template/base/apps => templates/frontend}/web-base/src/components/ui/sonner.tsx (100%) rename apps/cli/{template/base/apps => templates/frontend}/web-base/src/index.css (100%) rename apps/cli/{template/base/apps => templates/frontend}/web-base/src/lib/utils.ts (100%) diff --git a/.changeset/smart-days-argue.md b/.changeset/smart-days-argue.md new file mode 100644 index 0000000..fcc5f02 --- /dev/null +++ b/.changeset/smart-days-argue.md @@ -0,0 +1,5 @@ +--- +"create-better-t-stack": major +--- + +add orpc, make turborepo optional, use handlebarjs to scaffold template diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..063fafd --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "files.watcherExclude": { + "**/.git/objects/**": true, + "**/.git/subtree-cache/**": true, + "**/.hg/store/**": true, + "**/templates/**": true + } +} diff --git a/apps/cli/package.json b/apps/cli/package.json index 53e4d57..9a10075 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,71 +1,74 @@ { - "name": "create-better-t-stack", - "version": "1.13.2", - "description": "A modern CLI tool for scaffolding end-to-end type-safe TypeScript projects with best practices and customizable configurations", - "type": "module", - "license": "MIT", - "author": "Aman Varshney", - "bin": { - "create-better-t-stack": "dist/index.js" - }, - "files": [ - "template", - "dist" - ], - "keywords": [ - "better-t-stack", - "typescript", - "boilerplate", - "starter", - "cli", - "turborepo", - "trpc", - "better-auth", - "monorepo", - "fullstack", - "type-safety", - "react", - "react-native", - "expo", - "hono", - "elysia", - "drizzle", - "prisma", - "tanstack", - "tailwind", - "shadcn", - "pwa", - "tauri", - "biome" - ], - "repository": { - "type": "git", - "url": "git+https://github.com/AmanVarshney01/create-better-t-stack.git", - "directory": "apps/cli" - }, - "homepage": "https://better-t-stack.pages.dev/", - "scripts": { - "build": "tsup", - "dev": "tsup --watch", - "check-types": "tsc --noEmit", - "check": "biome check --write .", - "test": "vitest run", - "prepublishOnly": "npm run build" - }, - "dependencies": { - "@clack/prompts": "^0.10.1", - "consola": "^3.4.2", - "execa": "^8.0.1", - "fs-extra": "^11.3.0", - "gradient-string": "^3.0.0", - "picocolors": "^1.1.1", - "yargs": "^17.7.2" - }, - "devDependencies": { - "@types/fs-extra": "^11.0.4", - "@types/node": "^20.17.30", - "@types/yargs": "^17.0.33", - "tsup": "^8.4.0", - "typescript": "^5.8.3" - } + "name": "create-better-t-stack", + "version": "1.13.2", + "description": "A modern CLI tool for scaffolding end-to-end type-safe TypeScript projects with best practices and customizable configurations", + "type": "module", + "license": "MIT", + "author": "Aman Varshney", + "bin": { + "create-better-t-stack": "dist/index.js" + }, + "files": [ + "templates", + "dist" + ], + "keywords": [ + "better-t-stack", + "typescript", + "boilerplate", + "starter", + "cli", + "turborepo", + "trpc", + "better-auth", + "monorepo", + "fullstack", + "type-safety", + "react", + "react-native", + "expo", + "hono", + "elysia", + "drizzle", + "prisma", + "tanstack", + "tailwind", + "shadcn", + "pwa", + "tauri", + "biome" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/AmanVarshney01/create-better-t-stack.git", + "directory": "apps/cli" + }, + "homepage": "https://better-t-stack.pages.dev/", + "scripts": { + "build": "tsup", + "dev": "tsup --watch", + "check-types": "tsc --noEmit", + "check": "biome check --write .", + "test": "vitest run", + "prepublishOnly": "npm run build" + }, + "dependencies": { + "@clack/prompts": "^0.10.1", + "consola": "^3.4.2", + "execa": "^8.0.1", + "fs-extra": "^11.3.0", + "globby": "^14.1.0", + "gradient-string": "^3.0.0", + "handlebars": "^4.7.8", + "picocolors": "^1.1.1", + "yargs": "^17.7.2" + }, + "devDependencies": { + "@types/fs-extra": "^11.0.4", + "@types/globby": "^9.1.0", + "@types/node": "^20.17.30", + "@types/yargs": "^17.0.33", + "tsup": "^8.4.0", + "typescript": "^5.8.3" + } } diff --git a/apps/cli/src/constants.ts b/apps/cli/src/constants.ts index 829cdf2..7413fdd 100644 --- a/apps/cli/src/constants.ts +++ b/apps/cli/src/constants.ts @@ -17,10 +17,11 @@ export const DEFAULT_CONFIG: ProjectConfig = { examples: [], git: true, packageManager: getUserPkgManager(), - noInstall: false, + install: true, dbSetup: "none", backend: "hono", runtime: "bun", + api: "trpc", }; export const dependencyVersionMap = { @@ -69,10 +70,20 @@ export const dependencyVersionMap = { "@types/express": "^5.0.1", "@types/cors": "^2.8.17", + turbo: "^2.4.2", + ai: "^4.2.8", "@ai-sdk/google": "^1.2.3", "@prisma/extension-accelerate": "^1.3.0", + + "@orpc/server": "^1.0.3", + "@orpc/react-query": "^1.0.3", + "@orpc/client": "^1.0.3", + + "@trpc/tanstack-react-query": "^11.0.0", + "@trpc/server": "^11.0.0", + "@trpc/client": "^11.0.0", } as const; export type AvailableDependencies = keyof typeof dependencyVersionMap; diff --git a/apps/cli/src/helpers/addons-setup.ts b/apps/cli/src/helpers/addons-setup.ts index 072c11e..6578283 100644 --- a/apps/cli/src/helpers/addons-setup.ts +++ b/apps/cli/src/helpers/addons-setup.ts @@ -1,29 +1,30 @@ import path from "node:path"; import fs from "fs-extra"; -import { PKG_ROOT } from "../constants"; -import type { - ProjectAddons, - ProjectFrontend, - ProjectPackageManager, -} from "../types"; +import type { ProjectFrontend } from "../types"; import { addPackageDependency } from "../utils/add-package-deps"; import { setupStarlight } from "./starlight-setup"; import { setupTauri } from "./tauri-setup"; -export async function setupAddons( - projectDir: string, - addons: ProjectAddons[], - packageManager: ProjectPackageManager, - frontends: ProjectFrontend[], -) { +import type { ProjectConfig } from "../types"; + +export async function setupAddons(config: ProjectConfig) { + const { projectName, addons, packageManager, frontend } = config; + const projectDir = path.resolve(process.cwd(), projectName); const hasWebFrontend = - frontends.includes("react-router") || frontends.includes("tanstack-router"); + frontend.includes("react-router") || frontend.includes("tanstack-router"); + + if (addons.includes("turborepo")) { + await addPackageDependency({ + devDependencies: ["turbo"], + projectDir, + }); + } if (addons.includes("pwa") && hasWebFrontend) { - await setupPwa(projectDir, frontends); + await setupPwa(projectDir, frontend); } if (addons.includes("tauri") && hasWebFrontend) { - await setupTauri(projectDir, packageManager, frontends); + await setupTauri(config); } if (addons.includes("biome")) { await setupBiome(projectDir); @@ -32,7 +33,7 @@ export async function setupAddons( await setupHusky(projectDir); } if (addons.includes("starlight")) { - await setupStarlight(projectDir, packageManager); + await setupStarlight(config); } } @@ -44,12 +45,7 @@ export function getWebAppDir( } async function setupBiome(projectDir: string) { - const biomeTemplateDir = path.join(PKG_ROOT, "template/with-biome"); - if (await fs.pathExists(biomeTemplateDir)) { - await fs.copy(biomeTemplateDir, projectDir, { overwrite: true }); - } - - addPackageDependency({ + await addPackageDependency({ devDependencies: ["@biomejs/biome"], projectDir, }); @@ -68,12 +64,7 @@ async function setupBiome(projectDir: string) { } async function setupHusky(projectDir: string) { - const huskyTemplateDir = path.join(PKG_ROOT, "template/with-husky"); - if (await fs.pathExists(huskyTemplateDir)) { - await fs.copy(huskyTemplateDir, projectDir, { overwrite: true }); - } - - addPackageDependency({ + await addPackageDependency({ devDependencies: ["husky", "lint-staged"], projectDir, }); @@ -98,81 +89,18 @@ async function setupHusky(projectDir: string) { } async function setupPwa(projectDir: string, frontends: ProjectFrontend[]) { - const pwaTemplateDir = path.join(PKG_ROOT, "template/with-pwa"); - if (await fs.pathExists(pwaTemplateDir)) { - await fs.copy(pwaTemplateDir, projectDir, { overwrite: true }); - } - const clientPackageDir = getWebAppDir(projectDir, frontends); if (!(await fs.pathExists(clientPackageDir))) { return; } - addPackageDependency({ + await addPackageDependency({ dependencies: ["vite-plugin-pwa"], devDependencies: ["@vite-pwa/assets-generator"], projectDir: clientPackageDir, }); - const viteConfigPath = path.join(clientPackageDir, "vite.config.ts"); - if (await fs.pathExists(viteConfigPath)) { - let viteConfig = await fs.readFile(viteConfigPath, "utf8"); - - if (!viteConfig.includes("vite-plugin-pwa")) { - const firstImportMatch = viteConfig.match( - /^import .* from ['"](.*)['"]/m, - ); - - if (firstImportMatch) { - viteConfig = viteConfig.replace( - firstImportMatch[0], - `import { VitePWA } from "vite-plugin-pwa";\n${firstImportMatch[0]}`, - ); - } else { - viteConfig = `import { VitePWA } from "vite-plugin-pwa";\n${viteConfig}`; - } - } - - const pwaPluginCode = `VitePWA({ - registerType: "autoUpdate", - manifest: { - name: "My App", - short_name: "My App", - description: "My App", - theme_color: "#0c0c0c", - }, - pwaAssets: { - disabled: false, - config: true, - }, - devOptions: { - enabled: true, - }, - })`; - - if (!viteConfig.includes("VitePWA(")) { - if (frontends.includes("react-router")) { - viteConfig = viteConfig.replace( - /plugins: \[\s*tailwindcss\(\)/, - `plugins: [\n tailwindcss(),\n ${pwaPluginCode}`, - ); - } else if (frontends.includes("tanstack-router")) { - viteConfig = viteConfig.replace( - /plugins: \[\s*tailwindcss\(\)/, - `plugins: [\n tailwindcss(),\n ${pwaPluginCode}`, - ); - } else { - viteConfig = viteConfig.replace( - /plugins: \[/, - `plugins: [\n ${pwaPluginCode},`, - ); - } - } - - await fs.writeFile(viteConfigPath, viteConfig); - } - const clientPackageJsonPath = path.join(clientPackageDir, "package.json"); if (await fs.pathExists(clientPackageJsonPath)) { const packageJson = await fs.readJson(clientPackageJsonPath); diff --git a/apps/cli/src/helpers/api-setup.ts b/apps/cli/src/helpers/api-setup.ts new file mode 100644 index 0000000..216f323 --- /dev/null +++ b/apps/cli/src/helpers/api-setup.ts @@ -0,0 +1,37 @@ +import * as path from "node:path"; +import type { ProjectApi, ProjectConfig } from "../types"; +import { addPackageDependency } from "../utils/add-package-deps"; + +export async function setupApi(config: ProjectConfig): Promise { + if (config.api === "none") return; + const { api, projectName } = config; + const projectDir = path.resolve(process.cwd(), projectName); + const webDir = path.join(projectDir, "apps/web"); + const serverDir = path.join(projectDir, "apps/server"); + + if (api === "orpc") { + await addPackageDependency({ + dependencies: ["@orpc/react-query", "@orpc/server", "@orpc/client"], + projectDir: webDir, + }); + await addPackageDependency({ + dependencies: ["@orpc/server", "@orpc/client"], + projectDir: serverDir, + }); + } + + if (api === "trpc") { + await addPackageDependency({ + dependencies: [ + "@trpc/tanstack-react-query", + "@trpc/server", + "@trpc/client", + ], + projectDir: webDir, + }); + await addPackageDependency({ + dependencies: ["@trpc/server", "@trpc/client"], + projectDir: serverDir, + }); + } +} diff --git a/apps/cli/src/helpers/auth-setup.ts b/apps/cli/src/helpers/auth-setup.ts index 261a294..d9d6555 100644 --- a/apps/cli/src/helpers/auth-setup.ts +++ b/apps/cli/src/helpers/auth-setup.ts @@ -15,40 +15,40 @@ export function generateAuthSecret(length = 32): string { return result; } -export async function setupAuth( - projectDir: string, - enableAuth: boolean, - frontends: ProjectFrontend[] = [], -): Promise { - if (!enableAuth) { +import type { ProjectConfig } from "../types"; + +export async function setupAuth(config: ProjectConfig): Promise { + const { projectName, auth, frontend } = config; + if (!auth) { return; } + const projectDir = path.resolve(process.cwd(), projectName); const serverDir = path.join(projectDir, "apps/server"); const clientDir = path.join(projectDir, "apps/web"); const nativeDir = path.join(projectDir, "apps/native"); try { - addPackageDependency({ + await addPackageDependency({ dependencies: ["better-auth"], projectDir: serverDir, }); if ( - frontends.includes("react-router") || - frontends.includes("tanstack-router") || - frontends.includes("tanstack-start") + frontend.includes("react-router") || + frontend.includes("tanstack-router") || + frontend.includes("tanstack-start") ) { - addPackageDependency({ + await addPackageDependency({ dependencies: ["better-auth"], projectDir: clientDir, }); } - if (frontends.includes("native")) { - addPackageDependency({ + if (frontend.includes("native")) { + await addPackageDependency({ dependencies: ["better-auth", "@better-auth/expo"], projectDir: nativeDir, }); - addPackageDependency({ + await addPackageDependency({ dependencies: ["@better-auth/expo"], projectDir: serverDir, }); diff --git a/apps/cli/src/helpers/backend-framework-setup.ts b/apps/cli/src/helpers/backend-framework-setup.ts index 6347951..4463ea1 100644 --- a/apps/cli/src/helpers/backend-framework-setup.ts +++ b/apps/cli/src/helpers/backend-framework-setup.ts @@ -3,11 +3,14 @@ import type { AvailableDependencies } from "../constants"; import type { ProjectBackend, ProjectRuntime } from "../types"; import { addPackageDependency } from "../utils/add-package-deps"; +import type { ProjectConfig } from "../types"; + export async function setupBackendDependencies( - projectDir: string, - framework: ProjectBackend, - runtime: ProjectRuntime, + config: ProjectConfig, ): Promise { + const { projectName, backend, runtime } = config; + const projectDir = path.resolve(process.cwd(), projectName); + const framework = backend; const serverDir = path.join(projectDir, "apps/server"); const dependencies: AvailableDependencies[] = []; @@ -40,7 +43,7 @@ export async function setupBackendDependencies( devDependencies.push("@types/bun"); } - addPackageDependency({ + await addPackageDependency({ dependencies, devDependencies, projectDir: serverDir, diff --git a/apps/cli/src/helpers/create-project.ts b/apps/cli/src/helpers/create-project.ts index e033cba..f5db2ad 100644 --- a/apps/cli/src/helpers/create-project.ts +++ b/apps/cli/src/helpers/create-project.ts @@ -1,9 +1,10 @@ import path from "node:path"; -import { cancel, spinner } from "@clack/prompts"; +import { cancel, log, spinner } from "@clack/prompts"; import fs from "fs-extra"; import pc from "picocolors"; import type { ProjectConfig } from "../types"; import { setupAddons } from "./addons-setup"; +import { setupApi } from "./api-setup"; import { setupAuth } from "./auth-setup"; import { setupBackendDependencies } from "./backend-framework-setup"; import { createReadme } from "./create-readme"; @@ -17,10 +18,13 @@ import { setupRuntime } from "./runtime-setup"; import { copyBaseTemplate, fixGitignoreFiles, + handleExtras, + setupAddonsTemplate, setupAuthTemplate, setupBackendFramework, + setupDbOrmTemplates, + setupExamplesTemplate, setupFrontendTemplates, - setupOrmTemplate, } from "./template-manager"; export async function createProject(options: ProjectConfig): Promise { @@ -30,74 +34,47 @@ export async function createProject(options: ProjectConfig): Promise { try { await fs.ensureDir(projectDir); - await copyBaseTemplate(projectDir); - await setupFrontendTemplates(projectDir, options.frontend); + await copyBaseTemplate(projectDir, options); + await setupFrontendTemplates(projectDir, options); - await fixGitignoreFiles(projectDir); + await setupBackendFramework(projectDir, options); + await setupBackendDependencies(options); - await setupBackendFramework(projectDir, options.backend); - await setupBackendDependencies( - projectDir, - options.backend, - options.runtime, - ); + await setupDbOrmTemplates(projectDir, options); - await setupOrmTemplate( - projectDir, - options.orm, - options.database, - options.auth, - ); + await setupDatabase(options); - await setupDatabase( - projectDir, - options.database, - options.orm, - options.packageManager, - options.dbSetup === "turso", - options.dbSetup === "prisma-postgres", - options.dbSetup === "mongodb-atlas", - options.dbSetup === "neon", - ); + await setupAuthTemplate(projectDir, options); + await setupAuth(options); - await setupAuthTemplate( - projectDir, - options.auth, - options.backend, - options.orm, - options.database, - options.frontend, - ); - await setupAuth(projectDir, options.auth, options.frontend); - - await setupRuntime(projectDir, options.runtime, options.backend); - - await setupExamples( - projectDir, - options.examples, - options.orm, - options.auth, - options.backend, - options.frontend, - ); - - await setupEnvironmentVariables(projectDir, options); - - await initializeGit(projectDir, options.git); - - if (options.addons.length > 0) { - await setupAddons( - projectDir, - options.addons, - options.packageManager, - options.frontend, - ); + await setupAddonsTemplate(projectDir, options); + if (options.addons.length > 0 && options.addons[0] !== "none") { + await setupAddons(options); } + await setupExamplesTemplate(projectDir, options); + await handleExtras(projectDir, options); + + if (options.examples.length > 0 && options.examples[0] !== "none") { + await setupExamples(options); + } + + await setupApi(options); + + await setupRuntime(options); + + await setupEnvironmentVariables(options); + await updatePackageConfigurations(projectDir, options); await createReadme(projectDir, options); - if (!options.noInstall) { + await initializeGit(projectDir, options.git); + + await fixGitignoreFiles(projectDir, options); + + log.success("Project template successfully scaffolded!"); + + if (options.install) { await installDependencies({ projectDir, packageManager: options.packageManager, @@ -105,22 +82,17 @@ export async function createProject(options: ProjectConfig): Promise { }); } - displayPostInstallInstructions( - options.database, - options.projectName, - options.packageManager, - !options.noInstall, - options.orm, - options.addons, - options.runtime, - options.frontend, - ); + displayPostInstallInstructions({ + ...options, + depsInstalled: options.install, + }); return projectDir; } catch (error) { - s.message(pc.red("Failed")); + s.stop(pc.red("Failed")); if (error instanceof Error) { cancel(pc.red(`Error during project creation: ${error.message}`)); + console.error(error.stack); process.exit(1); } throw error; diff --git a/apps/cli/src/helpers/db-setup.ts b/apps/cli/src/helpers/db-setup.ts index c3851fc..57572ee 100644 --- a/apps/cli/src/helpers/db-setup.ts +++ b/apps/cli/src/helpers/db-setup.ts @@ -10,50 +10,46 @@ import type { } from "../types"; import { addPackageDependency } from "../utils/add-package-deps"; import { setupMongoDBAtlas } from "./mongodb-atlas-setup"; -import { setupNeonPostgres } from "./neon-setup"; import { setupPrismaPostgres } from "./prisma-postgres-setup"; import { setupTurso } from "./turso-setup"; -export async function setupDatabase( - projectDir: string, - databaseType: ProjectDatabase, - orm: ProjectOrm, - packageManager: ProjectPackageManager, - setupTursoDb: boolean, - setupPrismaPostgresDb: boolean, - setupMongoDBAtlasDb: boolean, - setupNeonPostgresDb: boolean, -): Promise { +import { setupNeonPostgres } from "./neon-setup"; + +import type { ProjectConfig } from "../types"; + +export async function setupDatabase(config: ProjectConfig): Promise { + const { projectName, database, orm, packageManager, dbSetup } = config; + const projectDir = path.resolve(process.cwd(), projectName); const s = spinner(); const serverDir = path.join(projectDir, "apps/server"); - if (databaseType === "none") { + if (database === "none") { await fs.remove(path.join(serverDir, "src/db")); return; } try { if (orm === "prisma") { - addPackageDependency({ + await addPackageDependency({ dependencies: ["@prisma/client"], devDependencies: ["prisma"], projectDir: serverDir, }); } else if (orm === "drizzle") { - if (databaseType === "sqlite") { - addPackageDependency({ + if (database === "sqlite") { + await addPackageDependency({ dependencies: ["drizzle-orm", "@libsql/client"], devDependencies: ["drizzle-kit"], projectDir: serverDir, }); - } else if (databaseType === "postgres") { - addPackageDependency({ + } else if (database === "postgres") { + await addPackageDependency({ dependencies: ["drizzle-orm", "pg"], devDependencies: ["drizzle-kit", "@types/pg"], projectDir: serverDir, }); - } else if (databaseType === "mysql") { - addPackageDependency({ + } else if (database === "mysql") { + await addPackageDependency({ dependencies: ["drizzle-orm", "mysql2"], devDependencies: ["drizzle-kit"], projectDir: serverDir, @@ -61,16 +57,16 @@ export async function setupDatabase( } } - if (databaseType === "sqlite" && setupTursoDb) { - await setupTurso(projectDir, orm === "drizzle"); - } else if (databaseType === "postgres") { - if (orm === "prisma" && setupPrismaPostgresDb) { - await setupPrismaPostgres(projectDir, packageManager); - } else if (setupNeonPostgresDb) { - await setupNeonPostgres(projectDir, packageManager); + if (database === "sqlite" && dbSetup === "turso") { + await setupTurso(config); + } else if (database === "postgres") { + if (orm === "prisma" && dbSetup === "prisma-postgres") { + await setupPrismaPostgres(config); + } else if (dbSetup === "neon") { + await setupNeonPostgres(config); } - } else if (databaseType === "mongodb" && setupMongoDBAtlasDb) { - await setupMongoDBAtlas(projectDir); + } else if (database === "mongodb" && dbSetup === "mongodb-atlas") { + await setupMongoDBAtlas(config); } } catch (error) { s.stop(pc.red("Failed to set up database")); diff --git a/apps/cli/src/helpers/env-setup.ts b/apps/cli/src/helpers/env-setup.ts index b6ac2b8..d83f45b 100644 --- a/apps/cli/src/helpers/env-setup.ts +++ b/apps/cli/src/helpers/env-setup.ts @@ -42,9 +42,11 @@ async function addEnvVariablesToFile( } export async function setupEnvironmentVariables( - projectDir: string, - options: ProjectConfig, + config: ProjectConfig, ): Promise { + const { projectName } = config; + const projectDir = path.resolve(process.cwd(), projectName); + const options = config; const serverDir = path.join(projectDir, "apps/server"); const envPath = path.join(serverDir, ".env"); diff --git a/apps/cli/src/helpers/examples-setup.ts b/apps/cli/src/helpers/examples-setup.ts index f3c05c4..5ec8269 100644 --- a/apps/cli/src/helpers/examples-setup.ts +++ b/apps/cli/src/helpers/examples-setup.ts @@ -1,387 +1,35 @@ import path from "node:path"; import fs from "fs-extra"; import { PKG_ROOT } from "../constants"; -import type { ProjectBackend, ProjectFrontend, ProjectOrm } from "../types"; +import type { + ProjectBackend, + ProjectConfig, + ProjectFrontend, + ProjectOrm, +} from "../types"; import { addPackageDependency } from "../utils/add-package-deps"; -export async function setupExamples( - projectDir: string, - examples: string[], - orm: ProjectOrm, - auth: boolean, - backend: ProjectBackend, - frontend: ProjectFrontend[] = ["tanstack-router"], -): Promise { - const hasTanstackRouter = frontend.includes("tanstack-router"); - const hasTanstackStart = frontend.includes("tanstack-start"); - const hasReactRouter = frontend.includes("react-router"); - const hasWebFrontend = - hasTanstackRouter || hasReactRouter || hasTanstackStart; - - let routerType: string; - if (hasTanstackRouter) { - routerType = "web-tanstack-router"; - } else if (hasTanstackStart) { - routerType = "web-tanstack-start"; - } else { - routerType = "web-react-router"; - } - - const webAppExists = await fs.pathExists(path.join(projectDir, "apps/web")); - - if (examples.includes("todo") && hasWebFrontend && webAppExists) { - await setupTodoExample(projectDir, orm, auth, routerType); - } else { - await cleanupTodoFiles(projectDir, orm); - } - - if ( - examples.includes("ai") && - (backend === "hono" || backend === "express") && - hasWebFrontend && - webAppExists - ) { - await setupAIExample(projectDir, routerType); - } -} - -async function setupAIExample( - projectDir: string, - routerType: string, -): Promise { - const aiExampleDir = path.join(PKG_ROOT, "template/examples/ai"); - - if (await fs.pathExists(aiExampleDir)) { - const aiRouteSourcePath = path.join( - aiExampleDir, - `apps/${routerType}/src/routes/ai.tsx`, - ); - const aiRouteTargetPath = path.join( - projectDir, - "apps/web/src/routes/ai.tsx", - ); - - if (await fs.pathExists(aiRouteSourcePath)) { - await fs.copy(aiRouteSourcePath, aiRouteTargetPath, { overwrite: true }); - } - - await updateHeaderWithAILink(projectDir, routerType); +export async function setupExamples(config: ProjectConfig): Promise { + const { + projectName, + examples, + orm, + auth, + backend, + frontend = ["tanstack-router"], + } = config; + const projectDir = path.resolve(process.cwd(), projectName); + if (examples.includes("ai")) { const clientDir = path.join(projectDir, "apps/web"); - addPackageDependency({ + await addPackageDependency({ dependencies: ["ai"], projectDir: clientDir, }); - const serverDir = path.join(projectDir, "apps/server"); - addPackageDependency({ + await addPackageDependency({ dependencies: ["ai", "@ai-sdk/google"], projectDir: serverDir, }); - - await updateServerIndexWithAIRoute(projectDir); - } -} - -async function updateServerIndexWithAIRoute(projectDir: string): Promise { - const serverIndexPath = path.join(projectDir, "apps/server/src/index.ts"); - - if (await fs.pathExists(serverIndexPath)) { - let indexContent = await fs.readFile(serverIndexPath, "utf8"); - const isHono = indexContent.includes("hono"); - const isExpress = indexContent.includes("express"); - - if (isHono) { - const importSection = `import { streamText } from "ai";\nimport { google } from "@ai-sdk/google";\nimport { stream } from "hono/streaming";`; - - const aiRouteHandler = ` -// AI chat endpoint -app.post("/ai", async (c) => { - const body = await c.req.json(); - const messages = body.messages || []; - - const result = streamText({ - model: google("gemini-1.5-flash"), - messages, - }); - - c.header("X-Vercel-AI-Data-Stream", "v1"); - c.header("Content-Type", "text/plain; charset=utf-8"); - - return stream(c, (stream) => stream.pipe(result.toDataStream())); -});`; - - if (indexContent.includes("import {")) { - const lastImportIndex = indexContent.lastIndexOf("import"); - const endOfLastImport = indexContent.indexOf("\n", lastImportIndex); - indexContent = `${indexContent.substring(0, endOfLastImport + 1)} -${importSection} -${indexContent.substring(endOfLastImport + 1)}`; - } else { - indexContent = `${importSection} - -${indexContent}`; - } - - const trpcHandlerIndex = - indexContent.indexOf('app.use("/trpc"') || - indexContent.indexOf("app.use(trpc("); - if (trpcHandlerIndex !== -1) { - indexContent = `${indexContent.substring(0, trpcHandlerIndex)}${aiRouteHandler} - -${indexContent.substring(trpcHandlerIndex)}`; - } else { - const exportIndex = indexContent.indexOf("export default"); - if (exportIndex !== -1) { - indexContent = `${indexContent.substring(0, exportIndex)}${aiRouteHandler} - -${indexContent.substring(exportIndex)}`; - } else { - indexContent = `${indexContent} - -${aiRouteHandler}`; - } - } - } else if (isExpress) { - const importSection = `import { streamText } from "ai";\nimport { google } from "@ai-sdk/google";`; - - const aiRouteHandler = ` -// AI chat endpoint -app.post("/ai", async (req, res) => { - const { messages = [] } = req.body; - - const result = streamText({ - model: google("gemini-1.5-flash"), - messages, - }); - - result.pipeDataStreamToResponse(res); -});`; - - if ( - indexContent.includes("import {") || - indexContent.includes("import ") - ) { - const lastImportIndex = indexContent.lastIndexOf("import"); - const endOfLastImport = indexContent.indexOf("\n", lastImportIndex); - indexContent = `${indexContent.substring(0, endOfLastImport + 1)} -${importSection} -${indexContent.substring(endOfLastImport + 1)}`; - } else { - indexContent = `${importSection} - -${indexContent}`; - } - - const trpcHandlerIndex = indexContent.indexOf('app.use("/trpc"'); - if (trpcHandlerIndex !== -1) { - indexContent = `${indexContent.substring(0, trpcHandlerIndex)}${aiRouteHandler} - -${indexContent.substring(trpcHandlerIndex)}`; - } else { - const appListenIndex = indexContent.indexOf("app.listen("); - if (appListenIndex !== -1) { - const prevNewlineIndex = indexContent.lastIndexOf( - "\n", - appListenIndex, - ); - indexContent = `${indexContent.substring(0, prevNewlineIndex)}${aiRouteHandler} - -${indexContent.substring(prevNewlineIndex)}`; - } else { - indexContent = `${indexContent} - -${aiRouteHandler}`; - } - } - } - - await fs.writeFile(serverIndexPath, indexContent); - } -} - -async function updateHeaderWithAILink( - projectDir: string, - routerType: string, -): Promise { - const headerPath = path.join( - projectDir, - "apps/web/src/components/header.tsx", - ); - - if (await fs.pathExists(headerPath)) { - let headerContent = await fs.readFile(headerPath, "utf8"); - - const linksPattern = /const links = \[\s*([^;]*?)\s*\];/s; - const linksMatch = headerContent.match(linksPattern); - - if (linksMatch) { - const linksContent = linksMatch[1]; - if (!linksContent.includes('"/ai"')) { - const updatedLinks = `const links = [\n ${linksContent}${ - linksContent.trim().endsWith(",") ? "" : "," - }\n { to: "/ai", label: "AI Chat" },\n ];`; - - headerContent = headerContent.replace(linksPattern, updatedLinks); - await fs.writeFile(headerPath, headerContent); - } - } - } -} - -async function setupTodoExample( - projectDir: string, - orm: ProjectOrm, - auth: boolean, - routerType: string, -): Promise { - const todoExampleDir = path.join(PKG_ROOT, "template/examples/todo"); - - if (await fs.pathExists(todoExampleDir)) { - const todoRouteSourcePath = path.join( - todoExampleDir, - `apps/${routerType}/src/routes/todos.tsx`, - ); - const todoRouteTargetPath = path.join( - projectDir, - "apps/web/src/routes/todos.tsx", - ); - - if (await fs.pathExists(todoRouteSourcePath)) { - await fs.copy(todoRouteSourcePath, todoRouteTargetPath, { - overwrite: true, - }); - } - - if (orm !== "none") { - const todoRouterSourceFile = path.join( - todoExampleDir, - `apps/server/src/routers/with-${orm}-todo.ts`, - ); - const todoRouterTargetFile = path.join( - projectDir, - "apps/server/src/routers/todo.ts", - ); - - if (await fs.pathExists(todoRouterSourceFile)) { - await fs.copy(todoRouterSourceFile, todoRouterTargetFile, { - overwrite: true, - }); - } - - await updateRouterIndexToIncludeTodo(projectDir); - } - - await updateHeaderWithTodoLink(projectDir, routerType); - } -} - -async function updateRouterIndexToIncludeTodo( - projectDir: string, -): Promise { - const routerFile = path.join(projectDir, "apps/server/src/routers/index.ts"); - - if (await fs.pathExists(routerFile)) { - let routerContent = await fs.readFile(routerFile, "utf8"); - - if (!routerContent.includes("import { todoRouter }")) { - const lastImportIndex = routerContent.lastIndexOf("import"); - const endOfImports = routerContent.indexOf("\n\n", lastImportIndex); - - if (endOfImports !== -1) { - routerContent = `${routerContent.slice(0, endOfImports)} -import { todoRouter } from "./todo";${routerContent.slice(endOfImports)}`; - } else { - routerContent = `import { todoRouter } from "./todo";\n${routerContent}`; - } - - const routerDefIndex = routerContent.indexOf( - "export const appRouter = router({", - ); - if (routerDefIndex !== -1) { - const routerContentStart = - routerContent.indexOf("{", routerDefIndex) + 1; - routerContent = `${routerContent.slice(0, routerContentStart)} - todo: todoRouter,${routerContent.slice(routerContentStart)}`; - } - - await fs.writeFile(routerFile, routerContent); - } - } -} - -async function updateHeaderWithTodoLink( - projectDir: string, - routerType: string, -): Promise { - const headerPath = path.join( - projectDir, - "apps/web/src/components/header.tsx", - ); - - if (await fs.pathExists(headerPath)) { - let headerContent = await fs.readFile(headerPath, "utf8"); - - const linksPattern = /const links = \[\s*([^;]*?)\s*\];/s; - const linksMatch = headerContent.match(linksPattern); - - if (linksMatch) { - const linksContent = linksMatch[1]; - if (!linksContent.includes('"/todos"')) { - const updatedLinks = `const links = [\n ${linksContent}${ - linksContent.trim().endsWith(",") ? "" : "," - }\n { to: "/todos", label: "Todos" },\n ];`; - - headerContent = headerContent.replace(linksPattern, updatedLinks); - await fs.writeFile(headerPath, headerContent); - } - } - } -} - -async function cleanupTodoFiles( - projectDir: string, - orm: ProjectOrm, -): Promise { - if (orm === "drizzle") { - const todoSchemaFile = path.join( - projectDir, - "apps/server/src/db/schema/todo.ts", - ); - if (await fs.pathExists(todoSchemaFile)) { - await fs.remove(todoSchemaFile); - } - } else if (orm === "prisma") { - const todoPrismaFile = path.join( - projectDir, - "apps/server/prisma/schema/todo.prisma", - ); - if (await fs.pathExists(todoPrismaFile)) { - await fs.remove(todoPrismaFile); - } - } - - const todoRouterFile = path.join( - projectDir, - "apps/server/src/routers/todo.ts", - ); - if (await fs.pathExists(todoRouterFile)) { - await fs.remove(todoRouterFile); - } - - await updateRouterIndex(projectDir); -} - -async function updateRouterIndex(projectDir: string): Promise { - const routerFile = path.join(projectDir, "apps/server/src/routers/index.ts"); - - if (await fs.pathExists(routerFile)) { - let routerContent = await fs.readFile(routerFile, "utf8"); - routerContent = routerContent.replace( - /import { todoRouter } from ".\/todo";/, - "", - ); - routerContent = routerContent.replace(/todo: todoRouter,/, ""); - await fs.writeFile(routerFile, routerContent); } } diff --git a/apps/cli/src/helpers/mongodb-atlas-setup.ts b/apps/cli/src/helpers/mongodb-atlas-setup.ts index bfeef7a..742796b 100644 --- a/apps/cli/src/helpers/mongodb-atlas-setup.ts +++ b/apps/cli/src/helpers/mongodb-atlas-setup.ts @@ -4,6 +4,7 @@ import consola from "consola"; import { execa } from "execa"; import fs from "fs-extra"; import pc from "picocolors"; +import type { ProjectConfig } from "../types"; import { commandExists } from "../utils/command-exists"; type MongoDBConfig = { @@ -129,7 +130,9 @@ ${pc.green("MongoDB Atlas Manual Setup Instructions:")} `); } -export async function setupMongoDBAtlas(projectDir: string) { +export async function setupMongoDBAtlas(config: ProjectConfig) { + const { projectName } = config; + const projectDir = path.resolve(process.cwd(), projectName); const mainSpinner = spinner(); mainSpinner.start("Setting up MongoDB Atlas"); diff --git a/apps/cli/src/helpers/neon-setup.ts b/apps/cli/src/helpers/neon-setup.ts index 2c44c4b..923a0e3 100644 --- a/apps/cli/src/helpers/neon-setup.ts +++ b/apps/cli/src/helpers/neon-setup.ts @@ -5,6 +5,7 @@ import { execa } from "execa"; import fs from "fs-extra"; import pc from "picocolors"; import type { ProjectPackageManager } from "../types"; +import { getPackageExecutionCommand } from "../utils/get-package-execution-command"; type NeonConfig = { connectionString: string; @@ -13,41 +14,20 @@ type NeonConfig = { roleName: string; }; -function buildNeonCommand( - packageManager: string, - args: string[], -): { cmd: string; cmdArgs: string[] } { - let cmd: string; - let cmdArgs: string[]; - - switch (packageManager) { - case "pnpm": - cmd = "pnpm"; - cmdArgs = ["dlx", "neonctl", ...args]; - break; - case "bun": - cmd = "bunx"; - cmdArgs = ["neonctl", ...args]; - break; - default: - cmd = "npx"; - cmdArgs = ["neonctl", ...args]; - } - - return { cmd, cmdArgs }; -} - async function executeNeonCommand( - packageManager: string, - args: string[], + packageManager: ProjectPackageManager, + commandArgsString: string, spinnerText?: string, ) { - const s = spinnerText ? spinner() : null; + const s = spinner(); try { - const { cmd, cmdArgs } = buildNeonCommand(packageManager, args); + const fullCommand = getPackageExecutionCommand( + packageManager, + commandArgsString, + ); if (s) s.start(spinnerText); - const result = await execa(cmd, cmdArgs); + const result = await execa(fullCommand, { shell: true }); if (s) s.stop(spinnerText); return result; @@ -57,13 +37,10 @@ async function executeNeonCommand( } } -async function isNeonAuthenticated(packageManager: string) { +async function isNeonAuthenticated(packageManager: ProjectPackageManager) { try { - const { cmd, cmdArgs } = buildNeonCommand(packageManager, [ - "projects", - "list", - ]); - const result = await execa(cmd, cmdArgs); + const commandArgsString = "neonctl projects list"; + const result = await executeNeonCommand(packageManager, commandArgsString); return ( !result.stdout.includes("not authenticated") && !result.stdout.includes("error") @@ -73,11 +50,11 @@ async function isNeonAuthenticated(packageManager: string) { } } -async function authenticateWithNeon(packageManager: string) { +async function authenticateWithNeon(packageManager: ProjectPackageManager) { try { await executeNeonCommand( packageManager, - ["auth"], + "neonctl auth", "Authenticating with Neon...", ); log.success("Authenticated with Neon successfully!"); @@ -90,12 +67,13 @@ async function authenticateWithNeon(packageManager: string) { async function createNeonProject( projectName: string, - packageManager: string, + packageManager: ProjectPackageManager, ): Promise { try { + const commandArgsString = `neonctl projects create --name "${projectName}" --output json`; const { stdout } = await executeNeonCommand( packageManager, - ["projects", "create", "--name", projectName, "--output", "json"], + commandArgsString, `Creating Neon project "${projectName}"...`, ); @@ -150,10 +128,11 @@ function displayManualSetupInstructions() { DATABASE_URL="your_connection_string"`); } -export async function setupNeonPostgres( - projectDir: string, - packageManager: ProjectPackageManager, -) { +import type { ProjectConfig } from "../types"; + +export async function setupNeonPostgres(config: ProjectConfig): Promise { + const { projectName, packageManager } = config; + const projectDir = path.resolve(process.cwd(), projectName); const setupSpinner = spinner(); setupSpinner.start("Setting up Neon PostgreSQL"); diff --git a/apps/cli/src/helpers/post-installation.ts b/apps/cli/src/helpers/post-installation.ts index b8e5c52..87ea11a 100644 --- a/apps/cli/src/helpers/post-installation.ts +++ b/apps/cli/src/helpers/post-installation.ts @@ -9,18 +9,24 @@ import type { ProjectPackageManager, ProjectRuntime, } from "../types"; +import { getPackageExecutionCommand } from "../utils/get-package-execution-command"; + +import type { ProjectConfig } from "../types"; export function displayPostInstallInstructions( - database: ProjectDatabase, - projectName: string, - packageManager: ProjectPackageManager, - depsInstalled: boolean, - orm: ProjectOrm, - addons: ProjectAddons[], - runtime: ProjectRuntime, - frontends: ProjectFrontend[], - dbSetup?: ProjectDBSetup, + config: ProjectConfig & { depsInstalled: boolean }, ) { + const { + database, + projectName, + packageManager, + depsInstalled, + orm, + addons, + runtime, + frontend, + dbSetup, + } = config; const runCmd = packageManager === "npm" ? "npm run" : packageManager; const cdCmd = `cd ${projectName}`; const hasHuskyOrBiome = @@ -36,38 +42,59 @@ export function displayPostInstallInstructions( const lintingInstructions = hasHuskyOrBiome ? getLintingInstructions(runCmd) : ""; - const nativeInstructions = frontends?.includes("native") + const nativeInstructions = frontend?.includes("native") ? getNativeInstructions() : ""; const pwaInstructions = - addons?.includes("pwa") && frontends?.includes("react-router") + addons?.includes("pwa") && frontend?.includes("react-router") ? getPwaInstructions() : ""; const starlightInstructions = addons?.includes("starlight") ? getStarlightInstructions(runCmd) : ""; - const hasTanstackRouter = frontends?.includes("tanstack-router"); - const hasTanstackStart = frontends?.includes("tanstack-start"); - const hasReactRouter = frontends?.includes("react-router"); + const hasTanstackRouter = frontend?.includes("tanstack-router"); + const hasTanstackStart = frontend?.includes("tanstack-start"); + const hasReactRouter = frontend?.includes("react-router"); const hasWebFrontend = hasTanstackRouter || hasReactRouter || hasTanstackStart; - const hasNativeFrontend = frontends?.includes("native"); + const hasNativeFrontend = frontend?.includes("native"); const hasFrontend = hasWebFrontend || hasNativeFrontend; const webPort = hasReactRouter ? "5173" : "3001"; + const tazeCommand = getPackageExecutionCommand(packageManager, "taze -r"); consola.box( `${pc.bold("Next steps")}\n${pc.cyan("1.")} ${cdCmd} -${!depsInstalled ? `${pc.cyan("2.")} ${packageManager} install\n` : ""}${pc.cyan(depsInstalled ? "2." : "3.")} ${runCmd} dev +${ + !depsInstalled ? `${pc.cyan("2.")} ${packageManager} install\n` : "" +}${pc.cyan(depsInstalled ? "2." : "3.")} ${runCmd} dev ${pc.bold("Your project will be available at:")} ${ hasFrontend - ? `${hasWebFrontend ? `${pc.cyan("•")} Frontend: http://localhost:${webPort}\n` : ""}` - : `${pc.yellow("NOTE:")} You are creating a backend-only app (no frontend selected)\n` -}${pc.cyan("•")} API: http://localhost:3000 -${addons?.includes("starlight") ? `${pc.cyan("•")} Docs: http://localhost:4321\n` : ""}${nativeInstructions ? `\n${nativeInstructions.trim()}` : ""}${databaseInstructions ? `\n${databaseInstructions.trim()}` : ""}${tauriInstructions ? `\n${tauriInstructions.trim()}` : ""}${lintingInstructions ? `\n${lintingInstructions.trim()}` : ""}${pwaInstructions ? `\n${pwaInstructions.trim()}` : ""}${starlightInstructions ? `\n${starlightInstructions.trim()}` : ""} + ? `${ + hasWebFrontend + ? `${pc.cyan("•")} Frontend: http://localhost:${webPort}\n` + : "" + }` + : `${pc.yellow( + "NOTE:", + )} You are creating a backend-only app (no frontend selected)\n` +}${pc.cyan("•")} Backend: http://localhost:3000 +${ + addons?.includes("starlight") + ? `${pc.cyan("•")} Docs: http://localhost:4321\n` + : "" +}${nativeInstructions ? `\n${nativeInstructions.trim()}` : ""}${ + databaseInstructions ? `\n${databaseInstructions.trim()}` : "" +}${tauriInstructions ? `\n${tauriInstructions.trim()}` : ""}${ + lintingInstructions ? `\n${lintingInstructions.trim()}` : "" +}${pwaInstructions ? `\n${pwaInstructions.trim()}` : ""}${ + starlightInstructions ? `\n${starlightInstructions.trim()}` : "" +} + +${pc.bold("Update all dependencies:\n")}${pc.cyan(tazeCommand)} ${pc.bold("Like Better-T Stack?")} Please consider giving us a star on GitHub: ${pc.cyan("https://github.com/AmanVarshney01/create-better-t-stack")}`, @@ -75,11 +102,15 @@ ${pc.cyan("https://github.com/AmanVarshney01/create-better-t-stack")}`, } function getNativeInstructions(): string { - return `${pc.yellow("NOTE:")} For Expo connectivity issues, update apps/native/.env \nwith your local IP:\n${"EXPO_PUBLIC_SERVER_URL=http://192.168.0.103:3000"}\n`; + return `${pc.yellow( + "NOTE:", + )} For Expo connectivity issues, update apps/native/.env \nwith your local IP:\n${"EXPO_PUBLIC_SERVER_URL=http://192.168.0.103:3000"}\n`; } function getLintingInstructions(runCmd?: string): string { - return `${pc.bold("Linting and formatting:")}\n${pc.cyan("•")} Format and lint fix: ${`${runCmd} check`}\n\n`; + return `${pc.bold("Linting and formatting:")}\n${pc.cyan( + "•", + )} Format and lint fix: ${`${runCmd} check`}\n\n`; } function getDatabaseInstructions( @@ -93,14 +124,18 @@ function getDatabaseInstructions( if (orm === "prisma") { if (database === "sqlite") { instructions.push( - `${pc.yellow("NOTE:")} Turso support with Prisma is in Early Access and requires additional setup.`, + `${pc.yellow( + "NOTE:", + )} Turso support with Prisma is in Early Access and requires additional setup.`, `${"Learn more at: https://www.prisma.io/docs/orm/overview/databases/turso"}`, ); } if (runtime === "bun") { instructions.push( - `${pc.yellow("NOTE:")} Prisma with Bun may require additional configuration. If you encounter errors,\nfollow the guidance provided in the error messages`, + `${pc.yellow( + "NOTE:", + )} Prisma with Bun may require additional configuration. If you encounter errors,\nfollow the guidance provided in the error messages`, ); } @@ -117,13 +152,25 @@ function getDatabaseInstructions( } function getTauriInstructions(runCmd?: string): string { - return `\n${pc.bold("Desktop app with Tauri:")}\n${pc.cyan("•")} Start desktop app: ${`cd apps/web && ${runCmd} desktop:dev`}\n${pc.cyan("•")} Build desktop app: ${`cd apps/web && ${runCmd} desktop:build`}\n${pc.yellow("NOTE:")} Tauri requires Rust and platform-specific dependencies.\nSee: ${"https://v2.tauri.app/start/prerequisites/"}\n\n`; + return `\n${pc.bold("Desktop app with Tauri:")}\n${pc.cyan( + "•", + )} Start desktop app: ${`cd apps/web && ${runCmd} desktop:dev`}\n${pc.cyan( + "•", + )} Build desktop app: ${`cd apps/web && ${runCmd} desktop:build`}\n${pc.yellow( + "NOTE:", + )} Tauri requires Rust and platform-specific dependencies.\nSee: ${"https://v2.tauri.app/start/prerequisites/"}\n\n`; } function getPwaInstructions(): string { - return `${pc.bold("PWA with React Router v7:")}\n${pc.yellow("NOTE:")} There is a known compatibility issue between VitePWA and React Router v7.\nSee: https://github.com/vite-pwa/vite-plugin-pwa/issues/809\n`; + return `${pc.bold("PWA with React Router v7:")}\n${pc.yellow( + "NOTE:", + )} There is a known compatibility issue between VitePWA and React Router v7.\nSee: https://github.com/vite-pwa/vite-plugin-pwa/issues/809\n`; } function getStarlightInstructions(runCmd?: string): string { - return `${pc.bold("Documentation with Starlight:")}\n${pc.cyan("•")} Start docs site: ${`cd apps/docs && ${runCmd} dev`}\n${pc.cyan("•")} Build docs site: ${`cd apps/docs && ${runCmd} build`}\n`; + return `${pc.bold("Documentation with Starlight:")}\n${pc.cyan( + "•", + )} Start docs site: ${`cd apps/docs && ${runCmd} dev`}\n${pc.cyan( + "•", + )} Build docs site: ${`cd apps/docs && ${runCmd} build`}\n`; } diff --git a/apps/cli/src/helpers/prisma-postgres-setup.ts b/apps/cli/src/helpers/prisma-postgres-setup.ts index 74b8de2..32f0435 100644 --- a/apps/cli/src/helpers/prisma-postgres-setup.ts +++ b/apps/cli/src/helpers/prisma-postgres-setup.ts @@ -6,6 +6,7 @@ import fs from "fs-extra"; import pc from "picocolors"; import type { ProjectPackageManager } from "../types"; import { addPackageDependency } from "../utils/add-package-deps"; +import { getPackageExecutionCommand } from "../utils/get-package-execution-command"; type PrismaConfig = { databaseUrl: string; @@ -22,18 +23,17 @@ async function initPrismaDatabase( const prismaDir = path.join(serverDir, "prisma"); await fs.ensureDir(prismaDir); - const initCmd = - packageManager === "npm" - ? "npx" - : packageManager === "pnpm" - ? "pnpm dlx" - : "bunx"; - s.stop("Initializing Prisma. Follow the prompts below:"); - await execa(initCmd, ["prisma", "init", "--db"], { + const prismaInitCommand = getPackageExecutionCommand( + packageManager, + "prisma init --db", + ); + + await execa(prismaInitCommand, { cwd: serverDir, stdio: "inherit", + shell: true, }); log.info( @@ -112,7 +112,7 @@ DATABASE_URL="your_database_url"`); async function addPrismaAccelerateExtension(serverDir: string) { try { - addPackageDependency({ + await addPackageDependency({ dependencies: ["@prisma/extension-accelerate"], projectDir: serverDir, }); @@ -152,10 +152,11 @@ export default prisma; } } -export async function setupPrismaPostgres( - projectDir: string, - packageManager: ProjectPackageManager = "npm", -) { +import type { ProjectConfig } from "../types"; + +export async function setupPrismaPostgres(config: ProjectConfig) { + const { projectName, packageManager } = config; + const projectDir = path.resolve(process.cwd(), projectName); const serverDir = path.join(projectDir, "apps/server"); const s = spinner(); s.start("Setting up Prisma PostgreSQL"); @@ -184,7 +185,9 @@ export async function setupPrismaPostgres( s.stop(pc.red("Prisma PostgreSQL setup failed")); consola.error( pc.red( - `Error during Prisma PostgreSQL setup: ${error instanceof Error ? error.message : String(error)}`, + `Error during Prisma PostgreSQL setup: ${ + error instanceof Error ? error.message : String(error) + }`, ), ); diff --git a/apps/cli/src/helpers/project-config.ts b/apps/cli/src/helpers/project-config.ts index cbb0714..e33ccf6 100644 --- a/apps/cli/src/helpers/project-config.ts +++ b/apps/cli/src/helpers/project-config.ts @@ -3,7 +3,6 @@ import { log } from "@clack/prompts"; import { $, execa } from "execa"; import fs from "fs-extra"; import pc from "picocolors"; -import { PKG_ROOT } from "../constants"; import type { ProjectConfig } from "../types"; export async function updatePackageConfigurations( @@ -23,24 +22,71 @@ async function updateRootPackageJson( const packageJson = await fs.readJson(rootPackageJsonPath); packageJson.name = options.projectName; + // Define script sets + const turboScripts = { + dev: "turbo dev", + build: "turbo build", + "check-types": "turbo check-types", + "dev:native": "turbo -F native dev", + "dev:web": "turbo -F web dev", + "dev:server": "turbo -F server dev", + "db:push": "turbo -F server db:push", + "db:studio": "turbo -F server db:studio", + }; + + const pnpmScripts = { + dev: "pnpm -r --parallel dev", + build: "pnpm -r build", + "check-types": "pnpm -r check-types", + "dev:native": "pnpm --filter native dev", + "dev:web": "pnpm --filter web dev", + "dev:server": "pnpm --filter server dev", + "db:push": "pnpm --filter server db:push", + "db:studio": "pnpm --filter server db:studio", + }; + + const npmScripts = { + dev: "npm run dev --workspaces", + build: "npm run build --workspaces", + "check-types": "npm run check-types --workspaces", + "dev:native": "npm run dev --workspace native", + "dev:web": "npm run dev --workspace web", + "dev:server": "npm run dev --workspace server", + "db:push": "npm run db:push --workspace server", + "db:studio": "npm run db:studio --workspace server", + }; + + const bunScripts = { + dev: "bun run --filter '*' dev", + build: "bun run --filter '*' build", + "check-types": "bun run --filter '*' check-types", + "dev:native": "bun run --filter native dev", + "dev:web": "bun run --filter web dev", + "dev:server": "bun run --filter server dev", + "db:push": "bun run --filter server db:push", + "db:studio": "bun run --filter server db:studio", + }; + + if (options.addons.includes("turborepo")) { + packageJson.scripts = turboScripts; + } else { + if (options.packageManager === "pnpm") { + packageJson.scripts = pnpmScripts; + } else if (options.packageManager === "npm") { + packageJson.scripts = npmScripts; + } else if (options.packageManager === "bun") { + packageJson.scripts = bunScripts; + } else { + packageJson.scripts = {}; + } + } + const { stdout } = await execa(options.packageManager, ["-v"], { cwd: projectDir, }); packageJson.packageManager = `${options.packageManager}@${stdout.trim()}`; await fs.writeJson(rootPackageJsonPath, packageJson, { spaces: 2 }); - - if (options.packageManager === "pnpm") { - const pnpmWorkspaceTemplatePath = path.join( - PKG_ROOT, - "template/with-pnpm/pnpm-workspace.yaml", - ); - const targetWorkspacePath = path.join(projectDir, "pnpm-workspace.yaml"); - - if (await fs.pathExists(pnpmWorkspaceTemplatePath)) { - await fs.copy(pnpmWorkspaceTemplatePath, targetWorkspacePath); - } - } } } @@ -57,7 +103,7 @@ async function updateServerPackageJson( const serverPackageJson = await fs.readJson(serverPackageJsonPath); if (options.database !== "none") { - if (options.database === "sqlite") { + if (options.database === "sqlite" && options.orm === "drizzle") { serverPackageJson.scripts["db:local"] = "turso dev --db-file local.db"; } diff --git a/apps/cli/src/helpers/runtime-setup.ts b/apps/cli/src/helpers/runtime-setup.ts index 32cc212..771015e 100644 --- a/apps/cli/src/helpers/runtime-setup.ts +++ b/apps/cli/src/helpers/runtime-setup.ts @@ -1,44 +1,27 @@ import path from "node:path"; import fs from "fs-extra"; -import type { ProjectBackend, ProjectRuntime } from "../types"; +import type { ProjectBackend, ProjectConfig, ProjectRuntime } from "../types"; import { addPackageDependency } from "../utils/add-package-deps"; -export async function setupRuntime( - projectDir: string, - runtime: ProjectRuntime, - backendFramework: ProjectBackend, -): Promise { - if (backendFramework === "next") { +export async function setupRuntime(config: ProjectConfig): Promise { + const { projectName, runtime, backend } = config; + const projectDir = path.resolve(process.cwd(), projectName); + if (backend === "next") { return; } const serverDir = path.join(projectDir, "apps/server"); - const serverIndexPath = path.join(serverDir, "src/index.ts"); - - const indexContent = await fs.readFile(serverIndexPath, "utf-8"); if (runtime === "bun") { - await setupBunRuntime( - serverDir, - serverIndexPath, - indexContent, - backendFramework, - ); + await setupBunRuntime(serverDir, backend); } else if (runtime === "node") { - await setupNodeRuntime( - serverDir, - serverIndexPath, - indexContent, - backendFramework, - ); + await setupNodeRuntime(serverDir, backend); } } async function setupBunRuntime( serverDir: string, - serverIndexPath: string, - indexContent: string, - backendFramework: ProjectBackend, + backend: ProjectBackend, ): Promise { const packageJsonPath = path.join(serverDir, "package.json"); const packageJson = await fs.readJson(packageJsonPath); @@ -51,22 +34,15 @@ async function setupBunRuntime( await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 }); - addPackageDependency({ + await addPackageDependency({ devDependencies: ["@types/bun"], projectDir: serverDir, }); - - if (backendFramework === "hono") { - const updatedContent = `${indexContent}\n\nexport default app;\n`; - await fs.writeFile(serverIndexPath, updatedContent); - } } async function setupNodeRuntime( serverDir: string, - serverIndexPath: string, - indexContent: string, - backendFramework: ProjectBackend, + backend: ProjectBackend, ): Promise { const packageJsonPath = path.join(serverDir, "package.json"); const packageJson = await fs.readJson(packageJsonPath); @@ -79,62 +55,20 @@ async function setupNodeRuntime( await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 }); - addPackageDependency({ + await addPackageDependency({ devDependencies: ["tsx", "@types/node"], projectDir: serverDir, }); - if (backendFramework === "hono") { - addPackageDependency({ + if (backend === "hono") { + await addPackageDependency({ dependencies: ["@hono/node-server"], projectDir: serverDir, }); - - const importLine = 'import { serve } from "@hono/node-server";\n'; - const serverCode = ` -serve( - { - fetch: app.fetch, - port: 3000, - }, - (info) => { - console.log(\`Server is running on http://localhost:\${info.port}\`); - }, -);\n`; - - if (!indexContent.includes("@hono/node-server")) { - const importEndIndex = indexContent.lastIndexOf("import"); - const importSection = indexContent.substring(0, importEndIndex); - const restOfFile = indexContent.substring(importEndIndex); - - const updatedContent = - importSection + importLine + restOfFile + serverCode; - await fs.writeFile(serverIndexPath, updatedContent); - } - } else if (backendFramework === "elysia") { - addPackageDependency({ + } else if (backend === "elysia") { + await addPackageDependency({ dependencies: ["@elysiajs/node"], projectDir: serverDir, }); - - if (!indexContent.includes("@elysiajs/node")) { - const nodeImport = 'import { node } from "@elysiajs/node";\n'; - - const firstImportEnd = indexContent.indexOf( - "\n", - indexContent.indexOf("import"), - ); - const before = indexContent.substring(0, firstImportEnd + 1); - const after = indexContent.substring(firstImportEnd + 1); - - let updatedContent = before + nodeImport + after; - - updatedContent = updatedContent.replace( - /const app = new Elysia\([^)]*\)/, - "const app = new Elysia({ adapter: node() })", - ); - - await fs.writeFile(serverIndexPath, updatedContent); - } } } diff --git a/apps/cli/src/helpers/starlight-setup.ts b/apps/cli/src/helpers/starlight-setup.ts index 28ac8f5..5ae3c2f 100644 --- a/apps/cli/src/helpers/starlight-setup.ts +++ b/apps/cli/src/helpers/starlight-setup.ts @@ -1,42 +1,20 @@ import path from "node:path"; -import { log, spinner } from "@clack/prompts"; +import { spinner } from "@clack/prompts"; import consola from "consola"; import { execa } from "execa"; import pc from "picocolors"; -import type { ProjectPackageManager } from "../types"; +import type { ProjectConfig } from "../types"; +import { getPackageExecutionCommand } from "../utils/get-package-execution-command"; -export async function setupStarlight( - projectDir: string, - packageManager: ProjectPackageManager, -): Promise { +export async function setupStarlight(config: ProjectConfig): Promise { + const { projectName, packageManager } = config; + const projectDir = path.resolve(process.cwd(), projectName); const s = spinner(); try { - s.start("Setting up Starlight documentation site..."); + s.start("Setting up Starlight docs..."); - let cmd: string; - let args: string[]; - - switch (packageManager) { - case "npm": - cmd = "npx"; - args = ["create-astro@latest"]; - break; - case "pnpm": - cmd = "pnpm"; - args = ["dlx", "create-astro@latest"]; - break; - case "bun": - cmd = "bunx"; - args = ["create-astro@latest"]; - break; - default: - cmd = "npx"; - args = ["create-astro@latest"]; - } - - args = [ - ...args, + const starlightArgs = [ "docs", "--template", "starlight", @@ -46,17 +24,26 @@ export async function setupStarlight( "--no-git", "--skip-houston", ]; + const starlightArgsString = starlightArgs.join(" "); - await execa(cmd, args, { + const commandWithArgs = `create-astro@latest ${starlightArgsString}`; + + const starlightInitCommand = getPackageExecutionCommand( + packageManager, + commandWithArgs, + ); + + await execa(starlightInitCommand, { cwd: path.join(projectDir, "apps"), env: { CI: "true", }, + shell: true, }); - s.stop("Starlight documentation site setup successfully!"); + s.stop("Starlight docs setup successfully!"); } catch (error) { - s.stop(pc.red("Failed to set up Starlight documentation site")); + s.stop(pc.red("Failed to set up Starlight docs")); if (error instanceof Error) { consola.error(pc.red(error.message)); } diff --git a/apps/cli/src/helpers/tauri-setup.ts b/apps/cli/src/helpers/tauri-setup.ts index c8f0e05..62acf3f 100644 --- a/apps/cli/src/helpers/tauri-setup.ts +++ b/apps/cli/src/helpers/tauri-setup.ts @@ -1,17 +1,17 @@ import path from "node:path"; -import { log, spinner } from "@clack/prompts"; +import { spinner } from "@clack/prompts"; import { consola } from "consola"; import { execa } from "execa"; import fs from "fs-extra"; import pc from "picocolors"; -import type { ProjectFrontend, ProjectPackageManager } from "../types"; import { addPackageDependency } from "../utils/add-package-deps"; +import { getPackageExecutionCommand } from "../utils/get-package-execution-command"; -export async function setupTauri( - projectDir: string, - packageManager: ProjectPackageManager, - frontends: ProjectFrontend[], -): Promise { +import type { ProjectConfig } from "../types"; + +export async function setupTauri(config: ProjectConfig): Promise { + const { projectName, packageManager, frontend } = config; + const projectDir = path.resolve(process.cwd(), projectName); const s = spinner(); const clientPackageDir = path.join(projectDir, "apps/web"); @@ -22,7 +22,7 @@ export async function setupTauri( try { s.start("Setting up Tauri desktop app support..."); - addPackageDependency({ + await addPackageDependency({ devDependencies: ["@tauri-apps/cli"], projectDir: clientPackageDir, }); @@ -41,48 +41,35 @@ export async function setupTauri( await fs.writeJson(clientPackageJsonPath, packageJson, { spaces: 2 }); } - let cmd: string; - let args: string[]; - - switch (packageManager) { - case "npm": - cmd = "npx"; - args = ["@tauri-apps/cli@latest"]; - break; - case "pnpm": - cmd = "pnpm"; - args = ["dlx", "@tauri-apps/cli@latest"]; - break; - case "bun": - cmd = "bunx"; - args = ["@tauri-apps/cli@latest"]; - break; - default: - cmd = "npx"; - args = ["@tauri-apps/cli@latest"]; - } - - const hasReactRouter = frontends.includes("react-router"); + const hasReactRouter = frontend.includes("react-router"); const devUrl = hasReactRouter ? "http://localhost:5173" : "http://localhost:3001"; - args = [ - ...args, + const tauriArgs = [ "init", `--app-name=${path.basename(projectDir)}`, `--window-title=${path.basename(projectDir)}`, "--frontend-dist=../dist", `--dev-url=${devUrl}`, - `--before-dev-command=${packageManager} run dev`, - `--before-build-command=${packageManager} run build`, + `--before-dev-command=\"${packageManager} run dev\"`, + `--before-build-command=\"${packageManager} run build\"`, ]; + const tauriArgsString = tauriArgs.join(" "); - await execa(cmd, args, { + const commandWithArgs = `@tauri-apps/cli@latest ${tauriArgsString}`; + + const tauriInitCommand = getPackageExecutionCommand( + packageManager, + commandWithArgs, + ); + + await execa(tauriInitCommand, { cwd: clientPackageDir, env: { CI: "true", }, + shell: true, }); s.stop("Tauri desktop app support configured successfully!"); diff --git a/apps/cli/src/helpers/template-manager.ts b/apps/cli/src/helpers/template-manager.ts index 84c2835..a31942f 100644 --- a/apps/cli/src/helpers/template-manager.ts +++ b/apps/cli/src/helpers/template-manager.ts @@ -1,493 +1,538 @@ import path from "node:path"; +import consola from "consola"; import fs from "fs-extra"; +import { globby } from "globby"; +import pc from "picocolors"; import { PKG_ROOT } from "../constants"; -import type { - ProjectBackend, - ProjectDatabase, - ProjectFrontend, - ProjectOrm, -} from "../types"; -import { addPackageDependency } from "../utils/add-package-deps"; +import type { ProjectConfig } from "../types"; +import { processTemplate } from "../utils/template-processor"; -/** - * Copy base template structure but exclude app-specific folders that will be added based on options - */ -export async function copyBaseTemplate(projectDir: string): Promise { - const templateDir = path.join(PKG_ROOT, "template/base"); +async function processAndCopyFiles( + sourcePattern: string | string[], + baseSourceDir: string, + destDir: string, + context: ProjectConfig, + overwrite = true, +): Promise { + const sourceFiles = await globby(sourcePattern, { + cwd: baseSourceDir, + dot: true, + onlyFiles: true, + absolute: false, + }); - if (!(await fs.pathExists(templateDir))) { - throw new Error(`Template directory not found: ${templateDir}`); - } + for (const relativeSrcPath of sourceFiles) { + const srcPath = path.join(baseSourceDir, relativeSrcPath); + let relativeDestPath = relativeSrcPath; - await fs.ensureDir(projectDir); + if (relativeSrcPath.endsWith(".hbs")) { + relativeDestPath = relativeSrcPath.slice(0, -4); + } - const rootFiles = await fs.readdir(templateDir); - for (const file of rootFiles) { - const srcPath = path.join(templateDir, file); - const destPath = path.join(projectDir, file); + const destPath = path.join(destDir, relativeDestPath); - if (file === "apps") continue; + await fs.ensureDir(path.dirname(destPath)); - if (await fs.stat(srcPath).then((stat) => stat.isDirectory())) { - await fs.copy(srcPath, destPath); + if (srcPath.endsWith(".hbs")) { + await processTemplate(srcPath, destPath, context); } else { - await fs.copy(srcPath, destPath); + if (!overwrite && (await fs.pathExists(destPath))) { + continue; + } + await fs.copy(srcPath, destPath, { overwrite: true }); } } +} - await fs.ensureDir(path.join(projectDir, "apps")); - - const serverSrcDir = path.join(templateDir, "apps/server"); - const serverDestDir = path.join(projectDir, "apps/server"); - if (await fs.pathExists(serverSrcDir)) { - await fs.copy(serverSrcDir, serverDestDir); - } +export async function copyBaseTemplate( + projectDir: string, + context: ProjectConfig, +): Promise { + const templateDir = path.join(PKG_ROOT, "templates/base"); + await processAndCopyFiles( + ["package.json", "_gitignore"], + templateDir, + projectDir, + context, + ); } export async function setupFrontendTemplates( projectDir: string, - frontends: ProjectFrontend[], + context: ProjectConfig, ): Promise { - const hasTanstackWeb = frontends.includes("tanstack-router"); - const hasTanstackStart = frontends.includes("tanstack-start"); - const hasReactRouterWeb = frontends.includes("react-router"); - const hasNextWeb = frontends.includes("next"); - const hasNative = frontends.includes("native"); + const webFrontends = context.frontend.filter( + (f) => + f === "tanstack-router" || + f === "react-router" || + f === "tanstack-start" || + f === "next", + ); + const hasNative = context.frontend.includes("native"); - if (hasTanstackWeb || hasReactRouterWeb || hasTanstackStart || hasNextWeb) { - const webDir = path.join(projectDir, "apps/web"); - await fs.ensureDir(webDir); + if (webFrontends.length > 0) { + const webAppDir = path.join(projectDir, "apps/web"); + await fs.ensureDir(webAppDir); - const webBaseDir = path.join(PKG_ROOT, "template/base/apps/web-base"); + const webBaseDir = path.join(PKG_ROOT, "templates/frontend/web-base"); if (await fs.pathExists(webBaseDir)) { - await fs.copy(webBaseDir, webDir); + await processAndCopyFiles("**/*", webBaseDir, webAppDir, context); } - if (hasTanstackWeb) { - const frameworkDir = path.join( + for (const framework of webFrontends) { + const frameworkSrcDir = path.join( PKG_ROOT, - "template/base/apps/web-tanstack-router", + `templates/frontend/${framework}`, ); - if (await fs.pathExists(frameworkDir)) { - await fs.copy(frameworkDir, webDir, { overwrite: true }); - } - } else if (hasTanstackStart) { - const frameworkDir = path.join( - PKG_ROOT, - "template/base/apps/web-tanstack-start", - ); - if (await fs.pathExists(frameworkDir)) { - await fs.copy(frameworkDir, webDir, { overwrite: true }); - } - } else if (hasReactRouterWeb) { - const frameworkDir = path.join( - PKG_ROOT, - "template/base/apps/web-react-router", - ); - if (await fs.pathExists(frameworkDir)) { - await fs.copy(frameworkDir, webDir, { overwrite: true }); - } - } else if (hasNextWeb) { - const frameworkDir = path.join(PKG_ROOT, "template/base/apps/web-next"); - if (await fs.pathExists(frameworkDir)) { - await fs.copy(frameworkDir, webDir, { overwrite: true }); + if (await fs.pathExists(frameworkSrcDir)) { + await processAndCopyFiles("**/*", frameworkSrcDir, webAppDir, context); } } - const packageJsonPath = path.join(webDir, "package.json"); - if (await fs.pathExists(packageJsonPath)) { - const packageJson = await fs.readJson(packageJsonPath); - packageJson.name = "web"; - await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 }); + if (context.api !== "none") { + const webFramework = webFrontends[0]; + + const apiWebBaseDir = path.join( + PKG_ROOT, + `templates/api/${context.api}/web/base`, + ); + if (await fs.pathExists(apiWebBaseDir)) { + await processAndCopyFiles("**/*", apiWebBaseDir, webAppDir, context); + } + + const apiWebFrameworkDir = path.join( + PKG_ROOT, + `templates/api/${context.api}/web/${webFramework}`, + ); + if (await fs.pathExists(apiWebFrameworkDir)) { + await processAndCopyFiles( + "**/*", + apiWebFrameworkDir, + webAppDir, + context, + ); + } } } if (hasNative) { - const nativeSrcDir = path.join(PKG_ROOT, "template/base/apps/native"); - const nativeDestDir = path.join(projectDir, "apps/native"); + const nativeAppDir = path.join(projectDir, "apps/native"); + await fs.ensureDir(nativeAppDir); - if (await fs.pathExists(nativeSrcDir)) { - await fs.copy(nativeSrcDir, nativeDestDir); + const nativeBaseDir = path.join(PKG_ROOT, "templates/frontend/native"); + if (await fs.pathExists(nativeBaseDir)) { + await processAndCopyFiles("**/*", nativeBaseDir, nativeAppDir, context); } - await fs.writeFile( - path.join(projectDir, ".npmrc"), - "node-linker=hoisted\n", - ); + if (context.api !== "none") { + const apiNativeSrcDir = path.join( + PKG_ROOT, + `templates/api/${context.api}/native`, + ); + + if (await fs.pathExists(apiNativeSrcDir)) { + await processAndCopyFiles( + "**/*", + apiNativeSrcDir, + nativeAppDir, + context, + ); + } else { + } + } } } export async function setupBackendFramework( projectDir: string, - framework: ProjectBackend, + context: ProjectConfig, ): Promise { - if (framework === "next") { - const serverDir = path.join(projectDir, "apps/server"); - const nextTemplateDir = path.join( - PKG_ROOT, - "template/with-next/apps/server", + if ((context.backend as string) === "none") return; + + const serverAppDir = path.join(projectDir, "apps/server"); + await fs.ensureDir(serverAppDir); + + const serverBaseDir = path.join(PKG_ROOT, "templates/backend/server-base"); + if (await fs.pathExists(serverBaseDir)) { + await processAndCopyFiles("**/*", serverBaseDir, serverAppDir, context); + } else { + consola.warn( + pc.yellow(`Warning: server-base template not found at ${serverBaseDir}`), ); - - await fs.ensureDir(serverDir); - - if (await fs.pathExists(nextTemplateDir)) { - await fs.copy(nextTemplateDir, serverDir, { overwrite: true }); - - const packageJsonPath = path.join(serverDir, "package.json"); - if (await fs.pathExists(packageJsonPath)) { - const packageJson = await fs.readJson(packageJsonPath); - packageJson.name = "server"; - await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 }); - } - } - return; } - const frameworkDir = path.join(PKG_ROOT, `template/with-${framework}`); - if (await fs.pathExists(frameworkDir)) { - await fs.copy(frameworkDir, projectDir, { overwrite: true }); + const frameworkSrcDir = path.join( + PKG_ROOT, + `templates/backend/${context.backend}`, + ); + if (await fs.pathExists(frameworkSrcDir)) { + await processAndCopyFiles("**/*", frameworkSrcDir, serverAppDir, context); + } else { + consola.warn( + pc.yellow( + `Warning: Backend template directory not found, skipping: ${frameworkSrcDir}`, + ), + ); + } + + if (context.api !== "none") { + const apiServerBaseDir = path.join( + PKG_ROOT, + `templates/api/${context.api}/server/base`, + ); + if (await fs.pathExists(apiServerBaseDir)) { + await processAndCopyFiles( + "**/*", + apiServerBaseDir, + serverAppDir, + context, + ); + } + + const apiServerFrameworkDir = path.join( + PKG_ROOT, + `templates/api/${context.api}/server/${context.backend}`, + ); + if (await fs.pathExists(apiServerFrameworkDir)) { + await processAndCopyFiles( + "**/*", + apiServerFrameworkDir, + serverAppDir, + context, + ); + } } } -export async function setupOrmTemplate( +export async function setupDbOrmTemplates( projectDir: string, - orm: ProjectOrm, - database: ProjectDatabase, - auth: boolean, + context: ProjectConfig, ): Promise { - if (orm === "none" || database === "none") return; + if (context.orm === "none" || context.database === "none") return; - const ormTemplateDir = path.join(PKG_ROOT, getOrmTemplateDir(orm, database)); + const serverAppDir = path.join(projectDir, "apps/server"); + await fs.ensureDir(serverAppDir); - if (await fs.pathExists(ormTemplateDir)) { - await fs.copy(ormTemplateDir, projectDir, { overwrite: true }); + const dbOrmSrcDir = path.join( + PKG_ROOT, + `templates/db/${context.orm}/${context.database}`, + ); - if (!auth) { - if (orm === "prisma") { - const authSchemaPath = path.join( - projectDir, - "apps/server/prisma/schema/auth.prisma", - ); - if (await fs.pathExists(authSchemaPath)) { - await fs.remove(authSchemaPath); - } - } else if (orm === "drizzle") { - const authSchemaPath = path.join( - projectDir, - "apps/server/src/db/schema/auth.ts", - ); - if (await fs.pathExists(authSchemaPath)) { - await fs.remove(authSchemaPath); - } - } - } + if (await fs.pathExists(dbOrmSrcDir)) { + await processAndCopyFiles("**/*", dbOrmSrcDir, serverAppDir, context); + } else { + consola.warn( + pc.yellow( + `Warning: Database/ORM template directory not found, skipping: ${dbOrmSrcDir}`, + ), + ); } } export async function setupAuthTemplate( projectDir: string, - auth: boolean, - framework: ProjectBackend, - orm: ProjectOrm, - database: ProjectDatabase, - frontends: ProjectFrontend[], + context: ProjectConfig, ): Promise { - if (!auth) return; + if (!context.auth) return; - const authTemplateDir = path.join(PKG_ROOT, "template/with-auth"); - if (await fs.pathExists(authTemplateDir)) { - const hasReactRouter = frontends.includes("react-router"); - const hasTanStackRouter = frontends.includes("tanstack-router"); - const hasTanStackStart = frontends.includes("tanstack-start"); - const hasNextRouter = frontends.includes("next"); + const serverAppDir = path.join(projectDir, "apps/server"); + const webAppDir = path.join(projectDir, "apps/web"); + const nativeAppDir = path.join(projectDir, "apps/native"); + const webFrontends = context.frontend.filter( + (f) => + f === "tanstack-router" || + f === "react-router" || + f === "tanstack-start" || + f === "next", + ); + const hasNative = context.frontend.includes("native"); - if ( - hasReactRouter || - hasTanStackRouter || - hasTanStackStart || - hasNextRouter - ) { - const webDir = path.join(projectDir, "apps/web"); - - const webBaseAuthDir = path.join(authTemplateDir, "apps/web-base"); - if (await fs.pathExists(webBaseAuthDir)) { - await fs.copy(webBaseAuthDir, webDir, { overwrite: true }); - } - - if (hasReactRouter) { - const reactRouterAuthDir = path.join( - authTemplateDir, - "apps/web-react-router", - ); - if (await fs.pathExists(reactRouterAuthDir)) { - await fs.copy(reactRouterAuthDir, webDir, { overwrite: true }); - } - } - - if (hasTanStackRouter) { - const tanstackAuthDir = path.join( - authTemplateDir, - "apps/web-tanstack-router", - ); - if (await fs.pathExists(tanstackAuthDir)) { - await fs.copy(tanstackAuthDir, webDir, { overwrite: true }); - } - } - - if (hasTanStackStart) { - const tanstackStartAuthDir = path.join( - authTemplateDir, - "apps/web-tanstack-start", - ); - if (await fs.pathExists(tanstackStartAuthDir)) { - await fs.copy(tanstackStartAuthDir, webDir, { overwrite: true }); - } - } - - if (hasNextRouter) { - const nextAuthDir = path.join(authTemplateDir, "apps/web-next"); - if (await fs.pathExists(nextAuthDir)) { - await fs.copy(nextAuthDir, webDir, { overwrite: true }); - } - } + if (await fs.pathExists(serverAppDir)) { + const authServerBaseSrc = path.join(PKG_ROOT, "templates/auth/server/base"); + if (await fs.pathExists(authServerBaseSrc)) { + await processAndCopyFiles( + "**/*", + authServerBaseSrc, + serverAppDir, + context, + ); + } else { + consola.warn( + pc.yellow( + `Warning: Base auth server template not found at ${authServerBaseSrc}`, + ), + ); } - const serverAuthDir = path.join(authTemplateDir, "apps/server/src"); - const projectServerDir = path.join(projectDir, "apps/server/src"); - - await fs.copy( - path.join(serverAuthDir, "lib/trpc.ts"), - path.join(projectServerDir, "lib/trpc.ts"), - { overwrite: true }, - ); - - await fs.copy( - path.join(serverAuthDir, "routers/index.ts"), - path.join(projectServerDir, "routers/index.ts"), - { overwrite: true }, - ); - - if (framework === "next") { - if ( - await fs.pathExists( - path.join(authTemplateDir, "apps/server/src/with-next-app"), - ) - ) { - const nextAppAuthDir = path.join( - authTemplateDir, - "apps/server/src/with-next-app", - ); - const nextAppDestDir = path.join(projectDir, "apps/server/src/app"); - - await fs.ensureDir(nextAppDestDir); - - const files = await fs.readdir(nextAppAuthDir); - for (const file of files) { - const srcPath = path.join(nextAppAuthDir, file); - const destPath = path.join(nextAppDestDir, file); - await fs.copy(srcPath, destPath, { overwrite: true }); - } - } - - const contextFileName = "with-next-context.ts"; - await fs.copy( - path.join(serverAuthDir, "lib", contextFileName), - path.join(projectServerDir, "lib/context.ts"), - { overwrite: true }, + const authServerNextSrc = path.join(PKG_ROOT, "templates/auth/server/next"); + if (await fs.pathExists(authServerNextSrc)) { + await processAndCopyFiles( + "**/*", + authServerNextSrc, + serverAppDir, + context, ); + } else { + consola.warn( + pc.yellow( + `Warning: Next auth server template not found at ${authServerNextSrc}`, + ), + ); + } - const authLibFileName = getAuthLibDir(orm, database); - const authLibSourceDir = path.join(serverAuthDir, authLibFileName); - if (await fs.pathExists(authLibSourceDir)) { - const files = await fs.readdir(authLibSourceDir); - for (const file of files) { - await fs.copy( - path.join(authLibSourceDir, file), - path.join(projectServerDir, "lib", file), - { overwrite: true }, - ); - } + if (context.orm !== "none" && context.database !== "none") { + const orm = context.orm; + const db = context.database; + let authDbSrc = ""; + if (orm === "drizzle") { + authDbSrc = path.join( + PKG_ROOT, + `templates/auth/server/db/drizzle/${db}`, + ); + } else if (orm === "prisma") { + authDbSrc = path.join( + PKG_ROOT, + `templates/auth/server/db/prisma/${db}`, + ); + } + if (authDbSrc && (await fs.pathExists(authDbSrc))) { + await processAndCopyFiles("**/*", authDbSrc, serverAppDir, context); + } else { + consola.warn( + pc.yellow( + `Warning: Auth template for ${orm}/${db} not found at ${authDbSrc}`, + ), + ); + } + } + } else { + consola.warn( + pc.yellow( + "Warning: apps/server directory does not exist, skipping server-side auth setup.", + ), + ); + } + + if (webFrontends.length > 0 && (await fs.pathExists(webAppDir))) { + const authWebBaseSrc = path.join(PKG_ROOT, "templates/auth/web/base"); + if (await fs.pathExists(authWebBaseSrc)) { + await processAndCopyFiles("**/*", authWebBaseSrc, webAppDir, context); + } else { + consola.warn( + pc.yellow( + `Warning: Base auth web template not found at ${authWebBaseSrc}`, + ), + ); + } + + for (const framework of webFrontends) { + const authWebFrameworkSrc = path.join( + PKG_ROOT, + `templates/auth/web/${framework}`, + ); + if (await fs.pathExists(authWebFrameworkSrc)) { + await processAndCopyFiles( + "**/*", + authWebFrameworkSrc, + webAppDir, + context, + ); + } else { + consola.warn( + pc.yellow( + `Warning: Auth web template for ${framework} not found at ${authWebFrameworkSrc}`, + ), + ); + } + } + } + + if (hasNative && (await fs.pathExists(nativeAppDir))) { + const authNativeSrc = path.join(PKG_ROOT, "templates/auth/native"); + if (await fs.pathExists(authNativeSrc)) { + await processAndCopyFiles("**/*", authNativeSrc, nativeAppDir, context); + } else { + consola.warn( + pc.yellow( + `Warning: Auth native template not found at ${authNativeSrc}`, + ), + ); + } + } +} + +export async function setupAddonsTemplate( + projectDir: string, + context: ProjectConfig, +): Promise { + if (context.addons.includes("turborepo")) { + const turboSrcDir = path.join(PKG_ROOT, "templates/addons/turborepo"); + if (await fs.pathExists(turboSrcDir)) { + await processAndCopyFiles("**/*", turboSrcDir, projectDir, context); + } else { + consola.warn(pc.yellow("Warning: Turborepo addon template not found.")); + } + } + + if (context.addons.includes("husky")) { + const huskySrcDir = path.join(PKG_ROOT, "templates/addons/husky"); + if (await fs.pathExists(huskySrcDir)) { + await processAndCopyFiles("**/*", huskySrcDir, projectDir, context); + } else { + consola.warn(pc.yellow("Warning: Husky addon template not found.")); + } + } + + if (context.addons.includes("biome")) { + const biomeSrcDir = path.join(PKG_ROOT, "templates/addons/biome"); + if (await fs.pathExists(biomeSrcDir)) { + await processAndCopyFiles("**/*", biomeSrcDir, projectDir, context); + } else { + consola.warn(pc.yellow("Warning: Biome addon template not found.")); + } + } + + if (context.addons.includes("pwa")) { + const pwaSrcDir = path.join(PKG_ROOT, "templates/addons/pwa/apps/web"); + const webAppDir = path.join(projectDir, "apps/web"); + if (await fs.pathExists(pwaSrcDir)) { + if (await fs.pathExists(webAppDir)) { + await processAndCopyFiles("**/*", pwaSrcDir, webAppDir, context); + } else { + consola.warn( + pc.yellow( + "Warning: apps/web directory not found, cannot setup PWA addon.", + ), + ); } } else { - const contextFileName = `with-${framework}-context.ts`; - await fs.copy( - path.join(serverAuthDir, "lib", contextFileName), - path.join(projectServerDir, "lib/context.ts"), - { overwrite: true }, - ); + consola.warn(pc.yellow("Warning: PWA addon template not found.")); + } + } +} - const indexFileName = `with-${framework}-index.ts`; - await fs.copy( - path.join(serverAuthDir, indexFileName), - path.join(projectServerDir, "index.ts"), - { overwrite: true }, - ); +export async function setupExamplesTemplate( + projectDir: string, + context: ProjectConfig, +): Promise { + if (!context.examples || context.examples.length === 0) return; - const authLibFileName = getAuthLibDir(orm, database); - const authLibSourceDir = path.join(serverAuthDir, authLibFileName); - if (await fs.pathExists(authLibSourceDir)) { - const files = await fs.readdir(authLibSourceDir); - for (const file of files) { - await fs.copy( - path.join(authLibSourceDir, file), - path.join(projectServerDir, "lib", file), - { overwrite: true }, + const serverAppDir = path.join(projectDir, "apps/server"); + const webAppDir = path.join(projectDir, "apps/web"); + + for (const example of context.examples) { + const exampleBaseDir = path.join(PKG_ROOT, `templates/examples/${example}`); + + if (await fs.pathExists(serverAppDir)) { + const exampleServerSrc = path.join(exampleBaseDir, "server"); + if (await fs.pathExists(exampleServerSrc)) { + if (context.orm !== "none") { + const exampleOrmBaseSrc = path.join( + exampleServerSrc, + context.orm, + "base", ); + if (await fs.pathExists(exampleOrmBaseSrc)) { + await processAndCopyFiles( + "**/*", + exampleOrmBaseSrc, + serverAppDir, + context, + false, + ); + } + + if (context.database !== "none") { + const exampleDbSchemaSrc = path.join( + exampleServerSrc, + context.orm, + context.database, + ); + if (await fs.pathExists(exampleDbSchemaSrc)) { + await processAndCopyFiles( + "**/*", + exampleDbSchemaSrc, + serverAppDir, + context, + false, + ); + } + } } } } - if (frontends.includes("native")) { - const nativeAuthDir = path.join(authTemplateDir, "apps/native"); - const projectNativeDir = path.join(projectDir, "apps/native"); - - if (await fs.pathExists(nativeAuthDir)) { - await fs.copy(nativeAuthDir, projectNativeDir, { overwrite: true }); + if (await fs.pathExists(webAppDir)) { + const exampleWebSrc = path.join(exampleBaseDir, "web"); + if (await fs.pathExists(exampleWebSrc)) { + const webFrameworks = context.frontend.filter((f) => + [ + "next", + "react-router", + "tanstack-router", + "tanstack-start", + ].includes(f), + ); + for (const framework of webFrameworks) { + const exampleWebFrameworkSrc = path.join(exampleWebSrc, framework); + if (await fs.pathExists(exampleWebFrameworkSrc)) { + await processAndCopyFiles( + "**/*", + exampleWebFrameworkSrc, + webAppDir, + context, + false, + ); + } + } } - - addPackageDependency({ - dependencies: ["@better-auth/expo"], - projectDir: path.join(projectDir, "apps/server"), - }); - - await updateAuthConfigWithExpoPlugin(projectDir, orm, database); } } } -// Need to find a better way to handle this -async function updateAuthConfigWithExpoPlugin( +export async function fixGitignoreFiles( projectDir: string, - orm: ProjectOrm, - database: ProjectDatabase, + context: ProjectConfig, ): Promise { - const serverDir = path.join(projectDir, "apps/server"); + const gitignoreFiles = await globby(["**/.gitignore.hbs", "**/_gitignore"], { + cwd: projectDir, + dot: true, + onlyFiles: true, + absolute: true, + ignore: ["**/node_modules/**", "**/.git/**"], + }); - let authFilePath: string | undefined; - if (orm === "drizzle") { - if (database === "sqlite") { - authFilePath = path.join(serverDir, "src/lib/auth.ts"); - } else if (database === "postgres") { - authFilePath = path.join(serverDir, "src/lib/auth.ts"); - } - } else if (orm === "prisma") { - if (database === "sqlite") { - authFilePath = path.join(serverDir, "src/lib/auth.ts"); - } else if (database === "postgres") { - authFilePath = path.join(serverDir, "src/lib/auth.ts"); - } - } + for (const currentPath of gitignoreFiles) { + const dir = path.dirname(currentPath); + const filename = path.basename(currentPath); + const destPath = path.join(dir, ".gitignore"); - if (authFilePath && (await fs.pathExists(authFilePath))) { - let authFileContent = await fs.readFile(authFilePath, "utf8"); - - if (!authFileContent.includes("@better-auth/expo")) { - const importLine = 'import { expo } from "@better-auth/expo";\n'; - - const lastImportIndex = authFileContent.lastIndexOf("import"); - const afterLastImport = - authFileContent.indexOf("\n", lastImportIndex) + 1; - - authFileContent = - authFileContent.substring(0, afterLastImport) + - importLine + - authFileContent.substring(afterLastImport); - } - - if (!authFileContent.includes("plugins:")) { - authFileContent = authFileContent.replace( - /}\);/, - " plugins: [expo()],\n});", - ); - } else if (!authFileContent.includes("expo()")) { - authFileContent = authFileContent.replace( - /plugins: \[(.*?)\]/s, - (match, plugins) => { - return `plugins: [${plugins}${plugins.trim() ? ", " : ""}expo()]`; - }, - ); - } - - if (!authFileContent.includes("my-better-t-app://")) { - authFileContent = authFileContent.replace( - /trustedOrigins: \[(.*?)\]/s, - (match, origins) => { - return `trustedOrigins: [${origins}${origins.trim() ? ", " : ""}"my-better-t-app://"]`; - }, - ); - } - - await fs.writeFile(authFilePath, authFileContent); - } -} - -export async function fixGitignoreFiles(projectDir: string): Promise { - const gitignorePaths = await findGitignoreFiles(projectDir); - - for (const gitignorePath of gitignorePaths) { - if (await fs.pathExists(gitignorePath)) { - const targetPath = path.join(path.dirname(gitignorePath), ".gitignore"); - await fs.move(gitignorePath, targetPath, { overwrite: true }); - } - } -} - -/** - * Find all _gitignore files in the project recursively - */ -async function findGitignoreFiles(dir: string): Promise { - const gitignoreFiles: string[] = []; - - const gitignorePath = path.join(dir, "_gitignore"); - if (await fs.pathExists(gitignorePath)) { - gitignoreFiles.push(gitignorePath); - } - - try { - const entries = await fs.readdir(dir, { withFileTypes: true }); - - for (const entry of entries) { - if (entry.isDirectory() && entry.name !== "node_modules") { - const subDirPath = path.join(dir, entry.name); - const subDirFiles = await findGitignoreFiles(subDirPath); - gitignoreFiles.push(...subDirFiles); + try { + if (filename === ".gitignore.hbs") { + await processTemplate(currentPath, destPath, context); + await fs.remove(currentPath); + } else if (filename === "_gitignore") { + await fs.move(currentPath, destPath, { overwrite: true }); } + } catch (error) { + consola.error(`Error processing gitignore file ${currentPath}:`, error); } - } catch (error) {} - - return gitignoreFiles; + } } -function getOrmTemplateDir(orm: ProjectOrm, database: ProjectDatabase): string { - if (orm === "drizzle") { - if (database === "sqlite") return "template/with-drizzle-sqlite"; - if (database === "postgres") return "template/with-drizzle-postgres"; - if (database === "mysql") return "template/with-drizzle-mysql"; +export async function handleExtras( + projectDir: string, + context: ProjectConfig, +): Promise { + if (context.packageManager === "pnpm") { + const src = path.join(PKG_ROOT, "templates/extras/pnpm-workspace.yaml"); + const dest = path.join(projectDir, "pnpm-workspace.yaml"); + if (await fs.pathExists(src)) { + await fs.copy(src, dest); + } else { + consola.warn( + pc.yellow("Warning: pnpm-workspace.yaml template not found."), + ); + } } - - if (orm === "prisma") { - if (database === "sqlite") return "template/with-prisma-sqlite"; - if (database === "postgres") return "template/with-prisma-postgres"; - if (database === "mysql") return "template/with-prisma-mysql"; - if (database === "mongodb") return "template/with-prisma-mongodb"; - } - - return "template/base"; -} - -function getAuthLibDir(orm: ProjectOrm, database: ProjectDatabase): string { - if (orm === "drizzle") { - if (database === "sqlite") return "with-drizzle-sqlite-lib"; - if (database === "postgres") return "with-drizzle-postgres-lib"; - if (database === "mysql") return "with-drizzle-mysql-lib"; - } - - if (orm === "prisma") { - if (database === "sqlite") return "with-prisma-sqlite-lib"; - if (database === "postgres") return "with-prisma-postgres-lib"; - if (database === "mysql") return "with-prisma-mysql-lib"; - if (database === "mongodb") return "with-prisma-mongodb-lib"; - } - - throw new Error("Invalid ORM or database configuration for auth setup"); } diff --git a/apps/cli/src/helpers/turso-setup.ts b/apps/cli/src/helpers/turso-setup.ts index ec7aafa..56162f2 100644 --- a/apps/cli/src/helpers/turso-setup.ts +++ b/apps/cli/src/helpers/turso-setup.ts @@ -202,29 +202,22 @@ DATABASE_URL=your_database_url DATABASE_AUTH_TOKEN=your_auth_token`); } -export async function setupTurso( - projectDir: string, - shouldSetupTurso: boolean, -) { +import type { ProjectConfig } from "../types"; + +export async function setupTurso(config: ProjectConfig): Promise { + const { projectName, orm } = config; + const projectDir = path.resolve(process.cwd(), projectName); + const isDrizzle = orm === "drizzle"; const setupSpinner = spinner(); setupSpinner.start("Setting up Turso database"); try { - if (!shouldSetupTurso) { - setupSpinner.stop("Skipping Turso setup"); - await writeEnvFile(projectDir); - log.info( - pc.blue("Skipping Turso setup. Setting up empty configuration."), - ); - displayManualSetupInstructions(); - return; - } - const platform = os.platform(); const isMac = platform === "darwin"; - const canInstallCLI = platform !== "win32"; + const isLinux = platform === "linux"; + const isWindows = platform === "win32"; - if (!canInstallCLI) { + if (isWindows) { setupSpinner.stop(pc.yellow("Turso setup not supported on Windows")); log.warn(pc.yellow("Automatic Turso setup is not supported on Windows.")); await writeEnvFile(projectDir); diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts index e62a09e..f899535 100644 --- a/apps/cli/src/index.ts +++ b/apps/cli/src/index.ts @@ -8,6 +8,7 @@ import { createProject } from "./helpers/create-project"; import { gatherConfig } from "./prompts/config-prompts"; import type { ProjectAddons, + ProjectApi, ProjectBackend, ProjectConfig, ProjectDBSetup, @@ -17,33 +18,13 @@ import type { ProjectOrm, ProjectPackageManager, ProjectRuntime, + YargsArgv, } from "./types"; import { displayConfig } from "./utils/display-config"; import { generateReproducibleCommand } from "./utils/generate-reproducible-command"; import { getLatestCLIVersion } from "./utils/get-latest-cli-version"; import { renderTitle } from "./utils/render-title"; -type YargsArgv = { - projectDirectory?: string; - - yes?: boolean; - database?: ProjectDatabase; - orm?: ProjectOrm; - auth?: boolean; - frontend?: ProjectFrontend[]; - addons?: ProjectAddons[]; - examples?: ProjectExamples[]; - git?: boolean; - packageManager?: ProjectPackageManager; - install?: boolean; - dbSetup?: ProjectDBSetup; - backend?: ProjectBackend; - runtime?: ProjectRuntime; - - _: (string | number)[]; - $0: string; -}; - const exit = () => process.exit(0); process.on("SIGINT", exit); process.on("SIGTERM", exit); @@ -99,7 +80,15 @@ async function main() { type: "array", string: true, describe: "Additional addons", - choices: ["pwa", "tauri", "starlight", "biome", "husky", "none"], + choices: [ + "pwa", + "tauri", + "starlight", + "biome", + "husky", + "turborepo", + "none", + ], }) .option("examples", { type: "array", @@ -119,7 +108,7 @@ async function main() { }) .option("install", { type: "boolean", - describe: "Install dependencies (use --no-install to explicitly skip)", + describe: "Install dependencies", }) .option("db-setup", { type: "string", @@ -136,6 +125,11 @@ async function main() { describe: "Runtime", choices: ["bun", "node"], }) + .option("api", { + type: "string", + describe: "API type", + choices: ["trpc", "orpc"], + }) .completion() .recommendCommands() .version(getLatestCLIVersion()) @@ -188,7 +182,9 @@ async function main() { const elapsedTimeInSeconds = ((Date.now() - startTime) / 1000).toFixed(2); outro( pc.magenta( - `Project created successfully in ${pc.bold(elapsedTimeInSeconds)} seconds!`, + `Project created successfully in ${pc.bold( + elapsedTimeInSeconds, + )} seconds!`, ), ); } catch (error) { @@ -214,10 +210,10 @@ function processAndValidateFlags( ): Partial { const config: Partial = {}; + // --- Database and ORM validation --- if (options.database) { config.database = options.database as ProjectDatabase; } - if (options.orm) { if (options.orm === "none") { config.orm = "none"; @@ -225,7 +221,6 @@ function processAndValidateFlags( config.orm = options.orm as ProjectOrm; } } - if ( (config.database ?? options.database) === "mongodb" && (config.orm ?? options.orm) === "drizzle" @@ -332,7 +327,6 @@ function processAndValidateFlags( if (options.backend) { config.backend = options.backend as ProjectBackend; } - if (options.runtime) { config.runtime = options.runtime as ProjectRuntime; } @@ -353,12 +347,13 @@ function processAndValidateFlags( (f) => f === "tanstack-router" || f === "react-router" || - f === "tanstack-start", + f === "tanstack-start" || + f === "next", ); if (webFrontends.length > 1) { consola.fatal( - "Cannot select multiple web frameworks. Choose only one of: tanstack-router, tanstack-start, react-router", + "Cannot select multiple web frameworks. Choose only one of: tanstack-router, tanstack-start, react-router, next", ); process.exit(1); } @@ -366,6 +361,34 @@ function processAndValidateFlags( } } + if (options.api) { + config.api = options.api as ProjectApi; + } + + const effectiveFrontend = + config.frontend ?? + (options.frontend?.filter((f) => f !== "none") as ProjectFrontend[]) ?? + (options.yes ? DEFAULT_CONFIG.frontend : undefined); + + const includesNative = effectiveFrontend?.includes("native"); + + const effectiveApi = + config.api ?? (options.yes ? DEFAULT_CONFIG.api : undefined); + + if (includesNative && effectiveApi === "orpc") { + consola.fatal( + `oRPC API is not supported when using the 'native' frontend. Please use --api trpc or remove 'native' from --frontend.`, + ); + process.exit(1); + } + + if (includesNative && effectiveApi !== "trpc") { + if (!options.api || (options.yes && options.api !== "orpc")) { + config.api = "trpc"; + } + } + + // --- Addons validation --- if (options.addons && options.addons.length > 0) { if (options.addons.includes("none")) { if (options.addons.length > 1) { @@ -383,9 +406,6 @@ function processAndValidateFlags( webSpecificAddons.includes(addon), ); - const effectiveFrontend = - config.frontend ?? (options.yes ? DEFAULT_CONFIG.frontend : undefined); - const hasCompatibleWebFrontend = effectiveFrontend?.some( (f) => f === "tanstack-router" || f === "react-router", ); @@ -413,6 +433,7 @@ function processAndValidateFlags( } } + // --- Examples validation --- if (options.examples && options.examples.length > 0) { if (options.examples.includes("none")) { if (options.examples.length > 1) { @@ -437,25 +458,22 @@ function processAndValidateFlags( process.exit(1); } - const effectiveFrontend = - config.frontend ?? - (options.frontend?.filter((f) => f !== "none") as ProjectFrontend[]) ?? - (options.yes ? DEFAULT_CONFIG.frontend : undefined); - - const hasWebFrontend = effectiveFrontend?.some((f) => - ["tanstack-router", "react-router", "tanstack-start"].includes(f), + const hasWebFrontendForExamples = effectiveFrontend?.some((f) => + ["tanstack-router", "react-router", "tanstack-start", "next"].includes( + f, + ), ); - if (!hasWebFrontend) { + if (!hasWebFrontendForExamples) { if (options.frontend) { consola.fatal( - "Examples require a web frontend (tanstack-router, react-router, or tanstack-start). Cannot use --examples with your frontend selection.", + "Examples require a web frontend (tanstack-router, react-router, tanstack-start, or next). Cannot use --examples with your frontend selection.", ); process.exit(1); } else if (!options.yes) { } else { consola.fatal( - "Examples require a web frontend (tanstack-router, react-router, or tanstack-start) (default frontend incompatible).", + "Examples require a web frontend (tanstack-router, react-router, tanstack-start, or next) (default frontend incompatible).", ); process.exit(1); } @@ -465,18 +483,16 @@ function processAndValidateFlags( } } + // --- Other flags --- if (options.packageManager) { config.packageManager = options.packageManager as ProjectPackageManager; } - if (options.git !== undefined) { config.git = options.git; } - if (options.install !== undefined) { - config.noInstall = !options.install; + config.install = options.install; } - if (projectDirectory) { config.projectName = projectDirectory; } diff --git a/apps/cli/src/prompts/addons.ts b/apps/cli/src/prompts/addons.ts index 53679e6..34a41d0 100644 --- a/apps/cli/src/prompts/addons.ts +++ b/apps/cli/src/prompts/addons.ts @@ -29,6 +29,11 @@ export async function getAddonsChoice( label: "Husky", hint: "Add Git hooks with Husky, lint-staged (requires Biome)", }, + { + value: "turborepo" as const, + label: "Turborepo", + hint: "Optimize builds for monorepos", + }, ]; const webAddonOptions = [ diff --git a/apps/cli/src/prompts/api.ts b/apps/cli/src/prompts/api.ts new file mode 100644 index 0000000..ab1eef9 --- /dev/null +++ b/apps/cli/src/prompts/api.ts @@ -0,0 +1,58 @@ +import { cancel, isCancel, select } from "@clack/prompts"; +import pc from "picocolors"; +import { DEFAULT_CONFIG } from "../constants"; +import type { ProjectApi, ProjectFrontend } from "../types"; + +export async function getApiChoice( + Api?: ProjectApi | undefined, + frontend?: ProjectFrontend[], +): Promise { + if (Api) return Api; + + const includesNative = frontend?.includes("native"); + + let apiOptions = [ + { + value: "trpc" as const, + label: "tRPC", + hint: "End-to-end typesafe APIs made easy", + }, + { + value: "orpc" as const, + label: "oRPC", + hint: "End-to-end type-safe APIs that adhere to OpenAPI standards", + }, + { + value: "none" as const, + label: "None", + hint: "No API integration (skip API setup)", + }, + ]; + + if (includesNative) { + apiOptions = [ + { + value: "trpc" as const, + label: "tRPC", + hint: "End-to-end typesafe APIs made easy (Required for Native frontend)", + }, + ]; + } + + const apiType = await select({ + message: "Select API type", + options: apiOptions, + initialValue: includesNative ? "trpc" : DEFAULT_CONFIG.api, + }); + + if (isCancel(apiType)) { + cancel(pc.red("Operation cancelled")); + process.exit(0); + } + + if (includesNative && apiType !== "trpc") { + return "trpc"; + } + + return apiType; +} diff --git a/apps/cli/src/prompts/auth.ts b/apps/cli/src/prompts/auth.ts index b713275..d7ccf82 100644 --- a/apps/cli/src/prompts/auth.ts +++ b/apps/cli/src/prompts/auth.ts @@ -6,7 +6,6 @@ import type { ProjectFrontend } from "../types"; export async function getAuthChoice( auth: boolean | undefined, hasDatabase: boolean, - frontends?: ProjectFrontend[], ): Promise { if (!hasDatabase) return false; diff --git a/apps/cli/src/prompts/config-prompts.ts b/apps/cli/src/prompts/config-prompts.ts index cc9bc21..95822ef 100644 --- a/apps/cli/src/prompts/config-prompts.ts +++ b/apps/cli/src/prompts/config-prompts.ts @@ -1,7 +1,8 @@ -import { cancel, group, log } from "@clack/prompts"; +import { cancel, group } from "@clack/prompts"; import pc from "picocolors"; import type { ProjectAddons, + ProjectApi, ProjectBackend, ProjectConfig, ProjectDBSetup, @@ -13,6 +14,7 @@ import type { ProjectRuntime, } from "../types"; import { getAddonsChoice } from "./addons"; +import { getApiChoice } from "./api"; import { getAuthChoice } from "./auth"; import { getBackendFrameworkChoice } from "./backend-framework"; import { getDatabaseChoice } from "./database"; @@ -20,7 +22,7 @@ import { getDBSetupChoice } from "./db-setup"; import { getExamplesChoice } from "./examples"; import { getFrontendChoice } from "./frontend-option"; import { getGitChoice } from "./git"; -import { getNoInstallChoice } from "./install"; +import { getinstallChoice } from "./install"; import { getORMChoice } from "./orm"; import { getPackageManagerChoice } from "./package-manager"; import { getProjectName } from "./project-name"; @@ -35,11 +37,12 @@ type PromptGroupResults = { examples: ProjectExamples[]; git: boolean; packageManager: ProjectPackageManager; - noInstall: boolean; + install: boolean; dbSetup: ProjectDBSetup; backend: ProjectBackend; runtime: ProjectRuntime; frontend: ProjectFrontend[]; + api: ProjectApi; }; export async function gatherConfig( @@ -57,12 +60,9 @@ export async function gatherConfig( database: () => getDatabaseChoice(flags.database), orm: ({ results }) => getORMChoice(flags.orm, results.database !== "none", results.database), + api: ({ results }) => getApiChoice(flags.api, results.frontend), auth: ({ results }) => - getAuthChoice( - flags.auth, - results.database !== "none", - results.frontend, - ), + getAuthChoice(flags.auth, results.database !== "none"), addons: ({ results }) => getAddonsChoice(flags.addons, results.frontend), examples: ({ results }) => getExamplesChoice( @@ -79,7 +79,7 @@ export async function gatherConfig( ), git: () => getGitChoice(flags.git), packageManager: () => getPackageManagerChoice(flags.packageManager), - noInstall: () => getNoInstallChoice(flags.noInstall), + install: () => getinstallChoice(flags.install), }, { onCancel: () => { @@ -99,9 +99,10 @@ export async function gatherConfig( examples: result.examples, git: result.git, packageManager: result.packageManager, - noInstall: result.noInstall, + install: result.install, dbSetup: result.dbSetup, backend: result.backend, runtime: result.runtime, + api: result.api, }; } diff --git a/apps/cli/src/prompts/install.ts b/apps/cli/src/prompts/install.ts index ec62f27..2d17d1c 100644 --- a/apps/cli/src/prompts/install.ts +++ b/apps/cli/src/prompts/install.ts @@ -2,14 +2,12 @@ import { cancel, confirm, isCancel } from "@clack/prompts"; import pc from "picocolors"; import { DEFAULT_CONFIG } from "../constants"; -export async function getNoInstallChoice( - noInstall?: boolean, -): Promise { - if (noInstall !== undefined) return noInstall; +export async function getinstallChoice(install?: boolean): Promise { + if (install !== undefined) return install; const response = await confirm({ message: "Install dependencies?", - initialValue: !DEFAULT_CONFIG.noInstall, + initialValue: DEFAULT_CONFIG.install, }); if (isCancel(response)) { @@ -17,5 +15,5 @@ export async function getNoInstallChoice( process.exit(0); } - return !response; + return response; } diff --git a/apps/cli/src/types.ts b/apps/cli/src/types.ts index 4eeba01..06d5214 100644 --- a/apps/cli/src/types.ts +++ b/apps/cli/src/types.ts @@ -12,6 +12,7 @@ export type ProjectAddons = | "tauri" | "husky" | "starlight" + | "turborepo" | "none"; export type ProjectBackend = "hono" | "elysia" | "express" | "next"; export type ProjectRuntime = "node" | "bun"; @@ -29,6 +30,7 @@ export type ProjectDBSetup = | "mongodb-atlas" | "neon" | "none"; +export type ProjectApi = "trpc" | "orpc" | "none"; export interface ProjectConfig { projectName: string; @@ -41,23 +43,30 @@ export interface ProjectConfig { examples: ProjectExamples[]; git: boolean; packageManager: ProjectPackageManager; - noInstall: boolean; + install: boolean; dbSetup: ProjectDBSetup; frontend: ProjectFrontend[]; + api: ProjectApi; } -export type CLIOptions = { +export type YargsArgv = { + projectDirectory?: string; + yes?: boolean; - database?: string; - orm?: string; + database?: ProjectDatabase; + orm?: ProjectOrm; auth?: boolean; - frontend?: string[]; - addons?: string[]; - examples?: string[] | boolean; + frontend?: ProjectFrontend[]; + addons?: ProjectAddons[]; + examples?: ProjectExamples[]; git?: boolean; - packageManager?: string; + packageManager?: ProjectPackageManager; install?: boolean; - dbSetup?: string; - backend?: string; - runtime?: string; + dbSetup?: ProjectDBSetup; + backend?: ProjectBackend; + runtime?: ProjectRuntime; + api?: ProjectApi; + + _: (string | number)[]; + $0: string; }; diff --git a/apps/cli/src/utils/add-package-deps.ts b/apps/cli/src/utils/add-package-deps.ts index 498eba2..e83a176 100644 --- a/apps/cli/src/utils/add-package-deps.ts +++ b/apps/cli/src/utils/add-package-deps.ts @@ -3,30 +3,41 @@ import fs from "fs-extra"; import { type AvailableDependencies, dependencyVersionMap } from "../constants"; -export const addPackageDependency = (opts: { +export const addPackageDependency = async (opts: { dependencies?: AvailableDependencies[]; devDependencies?: AvailableDependencies[]; projectDir: string; -}) => { +}): Promise => { const { dependencies = [], devDependencies = [], projectDir } = opts; const pkgJsonPath = path.join(projectDir, "package.json"); - const pkgJson = fs.readJSONSync(pkgJsonPath); + + const pkgJson = await fs.readJson(pkgJsonPath); if (!pkgJson.dependencies) pkgJson.dependencies = {}; if (!pkgJson.devDependencies) pkgJson.devDependencies = {}; for (const pkgName of dependencies) { const version = dependencyVersionMap[pkgName]; - pkgJson.dependencies[pkgName] = version; + if (version) { + pkgJson.dependencies[pkgName] = version; + } else { + console.warn(`Warning: Dependency ${pkgName} not found in version map.`); + } } for (const pkgName of devDependencies) { const version = dependencyVersionMap[pkgName]; - pkgJson.devDependencies[pkgName] = version; + if (version) { + pkgJson.devDependencies[pkgName] = version; + } else { + console.warn( + `Warning: Dev dependency ${pkgName} not found in version map.`, + ); + } } - fs.writeJSONSync(pkgJsonPath, pkgJson, { + await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2, }); }; diff --git a/apps/cli/src/utils/display-config.ts b/apps/cli/src/utils/display-config.ts index c2c4cee..4d54cc1 100644 --- a/apps/cli/src/utils/display-config.ts +++ b/apps/cli/src/utils/display-config.ts @@ -2,66 +2,109 @@ import pc from "picocolors"; import type { ProjectConfig } from "../types"; export function displayConfig(config: Partial) { - const configDisplay = []; + const configDisplay: string[] = []; if (config.projectName) { configDisplay.push(`${pc.blue("Project Name:")} ${config.projectName}`); } if (config.frontend !== undefined) { + const frontend = Array.isArray(config.frontend) + ? config.frontend + : [config.frontend]; const frontendText = - config.frontend.length > 0 ? config.frontend.join(", ") : "none"; + frontend.length > 0 && frontend[0] !== undefined && frontend[0] !== "" + ? frontend.join(", ") + : "none"; configDisplay.push(`${pc.blue("Frontend:")} ${frontendText}`); } if (config.backend !== undefined) { - configDisplay.push(`${pc.blue("Backend Framework:")} ${config.backend}`); + configDisplay.push( + `${pc.blue("Backend Framework:")} ${String(config.backend)}`, + ); } if (config.runtime !== undefined) { - configDisplay.push(`${pc.blue("Runtime:")} ${config.runtime}`); + configDisplay.push(`${pc.blue("Runtime:")} ${String(config.runtime)}`); + } + + if (config.api !== undefined) { + configDisplay.push(`${pc.blue("API:")} ${String(config.api)}`); } if (config.database !== undefined) { - configDisplay.push(`${pc.blue("Database:")} ${config.database}`); + configDisplay.push(`${pc.blue("Database:")} ${String(config.database)}`); } if (config.orm !== undefined) { - configDisplay.push(`${pc.blue("ORM:")} ${config.orm}`); + configDisplay.push(`${pc.blue("ORM:")} ${String(config.orm)}`); } if (config.auth !== undefined) { - configDisplay.push(`${pc.blue("Authentication:")} ${config.auth}`); + const authText = + typeof config.auth === "boolean" + ? config.auth + ? "Yes" + : "No" + : String(config.auth); + configDisplay.push(`${pc.blue("Authentication:")} ${authText}`); } if (config.addons !== undefined) { + const addons = Array.isArray(config.addons) + ? config.addons + : [config.addons]; const addonsText = - config.addons.length > 0 ? config.addons.join(", ") : "none"; + addons.length > 0 && addons[0] !== undefined ? addons.join(", ") : "none"; configDisplay.push(`${pc.blue("Addons:")} ${addonsText}`); } if (config.examples !== undefined) { + const examples = Array.isArray(config.examples) + ? config.examples + : [config.examples]; const examplesText = - config.examples.length > 0 ? config.examples.join(", ") : "none"; + examples.length > 0 && examples[0] !== undefined + ? examples.join(", ") + : "none"; configDisplay.push(`${pc.blue("Examples:")} ${examplesText}`); } if (config.git !== undefined) { - configDisplay.push(`${pc.blue("Git Init:")} ${config.git}`); + const gitText = + typeof config.git === "boolean" + ? config.git + ? "Yes" + : "No" + : String(config.git); + configDisplay.push(`${pc.blue("Git Init:")} ${gitText}`); } if (config.packageManager !== undefined) { configDisplay.push( - `${pc.blue("Package Manager:")} ${config.packageManager}`, + `${pc.blue("Package Manager:")} ${String(config.packageManager)}`, ); } - if (config.noInstall !== undefined) { - configDisplay.push(`${pc.blue("Skip Install:")} ${config.noInstall}`); + if (config.install !== undefined) { + const installText = + typeof config.install === "boolean" + ? config.install + ? "Yes" + : "No" + : String(config.install); + configDisplay.push(`${pc.blue("Install Dependencies:")} ${installText}`); } if (config.dbSetup !== undefined) { - configDisplay.push(`${pc.blue("Database Setup:")} ${config.dbSetup}`); + configDisplay.push( + `${pc.blue("Database Setup:")} ${String(config.dbSetup)}`, + ); + } + + if (configDisplay.length === 0) { + return pc.yellow("No configuration selected."); } return configDisplay.join("\n"); diff --git a/apps/cli/src/utils/generate-reproducible-command.ts b/apps/cli/src/utils/generate-reproducible-command.ts index 1f23af3..13c9cd2 100644 --- a/apps/cli/src/utils/generate-reproducible-command.ts +++ b/apps/cli/src/utils/generate-reproducible-command.ts @@ -17,9 +17,13 @@ export function generateReproducibleCommand(config: ProjectConfig): string { } } + if (config.api) { + flags.push(`--api ${config.api}`); + } + flags.push(config.auth ? "--auth" : "--no-auth"); flags.push(config.git ? "--git" : "--no-git"); - flags.push(config.noInstall ? "--no-install" : "--install"); + flags.push(config.install ? "--install" : "--no-install"); if (config.runtime) { flags.push(`--runtime ${config.runtime}`); diff --git a/apps/cli/src/utils/get-package-execution-command.ts b/apps/cli/src/utils/get-package-execution-command.ts new file mode 100644 index 0000000..9501bfc --- /dev/null +++ b/apps/cli/src/utils/get-package-execution-command.ts @@ -0,0 +1,23 @@ +import type { ProjectPackageManager } from "../types"; + +/** + * Returns the appropriate command for running a package without installing it globally, + * based on the selected package manager. + * + * @param packageManager - The selected package manager (e.g., 'npm', 'yarn', 'pnpm', 'bun'). + * @param commandWithArgs - The command to run, including arguments (e.g., "prisma generate --schema=./prisma/schema.prisma"). + * @returns The full command string (e.g., "npx prisma generate --schema=./prisma/schema.prisma"). + */ +export function getPackageExecutionCommand( + packageManager: ProjectPackageManager | null | undefined, + commandWithArgs: string, +): string { + switch (packageManager) { + case "pnpm": + return `pnpm dlx ${commandWithArgs}`; + case "bun": + return `bunx ${commandWithArgs}`; + default: + return `npx ${commandWithArgs}`; + } +} diff --git a/apps/cli/src/utils/template-processor.ts b/apps/cli/src/utils/template-processor.ts new file mode 100644 index 0000000..560e124 --- /dev/null +++ b/apps/cli/src/utils/template-processor.ts @@ -0,0 +1,37 @@ +import path from "node:path"; +import fs from "fs-extra"; +import handlebars from "handlebars"; +import type { ProjectConfig } from "../types"; + +/** + * Processes a Handlebars template file and writes the output to the destination. + * @param srcPath Path to the source .hbs template file. + * @param destPath Path to write the processed file. + * @param context Data to be passed to the Handlebars template. + */ +export async function processTemplate( + srcPath: string, + destPath: string, + context: ProjectConfig, +): Promise { + try { + const templateContent = await fs.readFile(srcPath, "utf-8"); + const template = handlebars.compile(templateContent); + const processedContent = template(context); + + await fs.ensureDir(path.dirname(destPath)); + await fs.writeFile(destPath, processedContent); + } catch (error) { + console.error(`Error processing template ${srcPath}:`, error); + throw new Error(`Failed to process template ${srcPath}`); + } +} + +handlebars.registerHelper("or", (a, b) => a || b); + +handlebars.registerHelper("eq", (a, b) => a === b); + +handlebars.registerHelper( + "includes", + (array, value) => Array.isArray(array) && array.includes(value), +); diff --git a/apps/cli/template/base/apps/server/src/lib/trpc.ts b/apps/cli/template/base/apps/server/src/lib/trpc.ts deleted file mode 100644 index 44277fa..0000000 --- a/apps/cli/template/base/apps/server/src/lib/trpc.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { initTRPC, TRPCError } from "@trpc/server"; -import type { Context } from "./context"; - -export const t = initTRPC.context().create(); - -export const router = t.router; - -export const publicProcedure = t.procedure; diff --git a/apps/cli/template/base/apps/server/src/routers/index.ts b/apps/cli/template/base/apps/server/src/routers/index.ts deleted file mode 100644 index 91b444d..0000000 --- a/apps/cli/template/base/apps/server/src/routers/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { router, publicProcedure } from "../lib/trpc"; -import { todoRouter } from "./todo"; - -export const appRouter = router({ - healthCheck: publicProcedure.query(() => { - return "OK"; - }), - todo: todoRouter, -}); - -export type AppRouter = typeof appRouter; diff --git a/apps/cli/template/base/apps/web-base/src/utils/trpc.ts b/apps/cli/template/base/apps/web-base/src/utils/trpc.ts deleted file mode 100644 index 1c5bbb7..0000000 --- a/apps/cli/template/base/apps/web-base/src/utils/trpc.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { AppRouter } from "../../../server/src/routers"; -import { QueryCache, QueryClient } from "@tanstack/react-query"; -import { createTRPCClient, httpBatchLink } from "@trpc/client"; -import { createTRPCOptionsProxy } from "@trpc/tanstack-react-query"; -import { toast } from "sonner"; - -export const queryClient = new QueryClient({ - queryCache: new QueryCache({ - onError: (error) => { - toast.error(error.message, { - action: { - label: "retry", - onClick: () => { - queryClient.invalidateQueries(); - }, - }, - }); - }, - }), -}); - -export const trpcClient = createTRPCClient({ - links: [ - httpBatchLink({ - url: `${import.meta.env.VITE_SERVER_URL}/trpc`, - }), - ], -}); - -export const trpc = createTRPCOptionsProxy({ - client: trpcClient, - queryClient, -}); diff --git a/apps/cli/template/base/apps/web-next/src/components/header.tsx b/apps/cli/template/base/apps/web-next/src/components/header.tsx deleted file mode 100644 index 6ab5bbd..0000000 --- a/apps/cli/template/base/apps/web-next/src/components/header.tsx +++ /dev/null @@ -1,30 +0,0 @@ -"use client" -import Link from "next/link"; -import { ModeToggle } from "./mode-toggle"; - -export default function Header() { - const links = [ - { to: "/", label: "Home" }, - ]; - - return ( -
-
- -
- -
-
-
-
- ); -} diff --git a/apps/cli/template/base/apps/web-next/src/utils/trpc.ts b/apps/cli/template/base/apps/web-next/src/utils/trpc.ts deleted file mode 100644 index 61bd7a9..0000000 --- a/apps/cli/template/base/apps/web-next/src/utils/trpc.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { QueryCache, QueryClient } from '@tanstack/react-query'; -import { createTRPCClient, httpBatchLink } from '@trpc/client'; -import { createTRPCOptionsProxy } from '@trpc/tanstack-react-query'; -import type { AppRouter } from '../../../server/src/routers'; -import { toast } from 'sonner'; - -export const queryClient = new QueryClient({ - queryCache: new QueryCache({ - onError: (error) => { - toast.error(error.message, { - action: { - label: "retry", - onClick: () => { - queryClient.invalidateQueries(); - }, - }, - }); - }, - }), -}); - -const trpcClient = createTRPCClient({ - links: [ - httpBatchLink({ - url: `${process.env.NEXT_PUBLIC_SERVER_URL}/trpc`, - }), - ], -}) - -export const trpc = createTRPCOptionsProxy({ - client: trpcClient, - queryClient, -}); diff --git a/apps/cli/template/base/apps/web-react-router/src/components/header.tsx b/apps/cli/template/base/apps/web-react-router/src/components/header.tsx deleted file mode 100644 index 98a48e7..0000000 --- a/apps/cli/template/base/apps/web-react-router/src/components/header.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { NavLink } from "react-router"; -import { ModeToggle } from "./mode-toggle"; - -export default function Header() { - const links = [ - { to: "/", label: "Home" }, - ]; - - return ( -
-
- -
- -
-
-
-
- ); -} diff --git a/apps/cli/template/base/apps/web-react-router/vite.config.ts b/apps/cli/template/base/apps/web-react-router/vite.config.ts deleted file mode 100644 index 4a88d58..0000000 --- a/apps/cli/template/base/apps/web-react-router/vite.config.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { reactRouter } from "@react-router/dev/vite"; -import tailwindcss from "@tailwindcss/vite"; -import { defineConfig } from "vite"; -import tsconfigPaths from "vite-tsconfig-paths"; - -export default defineConfig({ - plugins: [tailwindcss(), reactRouter(), tsconfigPaths()], -}); diff --git a/apps/cli/template/base/apps/web-tanstack-router/src/components/header.tsx b/apps/cli/template/base/apps/web-tanstack-router/src/components/header.tsx deleted file mode 100644 index af279f7..0000000 --- a/apps/cli/template/base/apps/web-tanstack-router/src/components/header.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { Link } from "@tanstack/react-router"; -import { ModeToggle } from "./mode-toggle"; - -export default function Header() { - const links = [ - { to: "/", label: "Home" }, - ]; - - return ( -
-
- -
- -
-
-
-
- ); -} diff --git a/apps/cli/template/base/apps/web-tanstack-router/vite.config.ts b/apps/cli/template/base/apps/web-tanstack-router/vite.config.ts deleted file mode 100644 index 8b87133..0000000 --- a/apps/cli/template/base/apps/web-tanstack-router/vite.config.ts +++ /dev/null @@ -1,14 +0,0 @@ -import tailwindcss from "@tailwindcss/vite"; -import { TanStackRouterVite } from "@tanstack/router-plugin/vite"; -import react from "@vitejs/plugin-react"; -import path from "node:path"; -import { defineConfig } from "vite"; - -export default defineConfig({ - plugins: [tailwindcss(), TanStackRouterVite({}), react()], - resolve: { - alias: { - "@": path.resolve(__dirname, "./src"), - }, - }, -}); diff --git a/apps/cli/template/base/apps/web-tanstack-start/src/components/header.tsx b/apps/cli/template/base/apps/web-tanstack-start/src/components/header.tsx deleted file mode 100644 index a315561..0000000 --- a/apps/cli/template/base/apps/web-tanstack-start/src/components/header.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { Link } from "@tanstack/react-router"; - -export default function Header() { - const links = [ - { to: "/", label: "Home" }, - ]; - - return ( -
-
- -
-
-
- ); -} diff --git a/apps/cli/template/base/apps/web-tanstack-start/src/router.tsx b/apps/cli/template/base/apps/web-tanstack-start/src/router.tsx deleted file mode 100644 index eaea17d..0000000 --- a/apps/cli/template/base/apps/web-tanstack-start/src/router.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { - QueryCache, - QueryClient, - QueryClientProvider, -} from "@tanstack/react-query"; -import { createRouter as createTanstackRouter } from "@tanstack/react-router"; -import { createTRPCClient, httpBatchLink } from "@trpc/client"; -import { createTRPCOptionsProxy } from "@trpc/tanstack-react-query"; -import { toast } from "sonner"; -import type { AppRouter } from "../../server/src/routers"; -import Loader from "./components/loader"; -import "./index.css"; -import { routeTree } from "./routeTree.gen"; -import { TRPCProvider } from "./utils/trpc"; - -export const queryClient = new QueryClient({ - queryCache: new QueryCache({ - onError: (error) => { - toast.error(error.message, { - action: { - label: "retry", - onClick: () => { - queryClient.invalidateQueries(); - }, - }, - }); - }, - }), - defaultOptions: { queries: { staleTime: 60 * 1000 } }, -}); - -const trpcClient = createTRPCClient({ - links: [ - httpBatchLink({ - url: `${import.meta.env.VITE_SERVER_URL}/trpc`, - }), - ], -}); - -const trpc = createTRPCOptionsProxy({ - client: trpcClient, - queryClient: queryClient, -}); - -export const createRouter = () => { - const router = createTanstackRouter({ - routeTree, - scrollRestoration: true, - defaultPreloadStaleTime: 0, - context: { trpc, queryClient }, - defaultPendingComponent: () => , - defaultNotFoundComponent: () =>
Not Found
, - Wrap: ({ children }) => ( - - - {children} - - - ), - }); - - return router; -}; - -// Register the router instance for type safety -declare module "@tanstack/react-router" { - interface Register { - router: ReturnType; - } -} diff --git a/apps/cli/template/base/apps/web-tanstack-start/src/utils/trpc.ts b/apps/cli/template/base/apps/web-tanstack-start/src/utils/trpc.ts deleted file mode 100644 index 307a96b..0000000 --- a/apps/cli/template/base/apps/web-tanstack-start/src/utils/trpc.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { createTRPCContext } from "@trpc/tanstack-react-query"; -import type { AppRouter } from "../../../server/src/routers"; - -export const { TRPCProvider, useTRPC, useTRPCClient } = - createTRPCContext(); diff --git a/apps/cli/template/base/package.json b/apps/cli/template/base/package.json deleted file mode 100644 index a83c2b7..0000000 --- a/apps/cli/template/base/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "better-t-stack", - "private": true, - "workspaces": ["apps/*"], - "scripts": { - "dev": "turbo dev", - "build": "turbo build", - "check-types": "turbo check-types", - "dev:native": "turbo -F native dev", - "dev:web": "turbo -F web dev", - "dev:server": "turbo -F server dev", - "db:push": "turbo -F server db:push", - "db:studio": "turbo -F server db:studio" - }, - "devDependencies": { - "turbo": "^2.4.2" - } -} diff --git a/apps/cli/template/examples/todo/apps/server/src/routers/with-drizzle-todo.ts b/apps/cli/template/examples/todo/apps/server/src/routers/with-drizzle-todo.ts deleted file mode 100644 index 52a1b1b..0000000 --- a/apps/cli/template/examples/todo/apps/server/src/routers/with-drizzle-todo.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { z } from "zod"; -import { router, publicProcedure } from "../lib/trpc"; -import { todo } from "../db/schema/todo"; -import { eq } from "drizzle-orm"; -import { db } from "../db"; - -export const todoRouter = router({ - getAll: publicProcedure.query(async () => { - return await db.select().from(todo); - }), - - create: publicProcedure - .input(z.object({ text: z.string().min(1) })) - .mutation(async ({ input }) => { - return await db.insert(todo).values({ - text: input.text, - }); - }), - - toggle: publicProcedure - .input(z.object({ id: z.number(), completed: z.boolean() })) - .mutation(async ({ input }) => { - return await db - .update(todo) - .set({ completed: input.completed }) - .where(eq(todo.id, input.id)); - }), - - delete: publicProcedure - .input(z.object({ id: z.number() })) - .mutation(async ({ input }) => { - return await db.delete(todo).where(eq(todo.id, input.id)); - }), -}); diff --git a/apps/cli/template/with-auth/apps/server/src/lib/with-elysia-context.ts b/apps/cli/template/with-auth/apps/server/src/lib/with-elysia-context.ts deleted file mode 100644 index b97faf5..0000000 --- a/apps/cli/template/with-auth/apps/server/src/lib/with-elysia-context.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { Context as ElysiaContext } from "elysia"; -import { auth } from "./auth"; - -export type CreateContextOptions = { - context: ElysiaContext; -}; - -export async function createContext({ context }: CreateContextOptions) { - const session = await auth.api.getSession({ - headers: context.request.headers, - }); - - return { - session, - }; -} - -export type Context = Awaited>; diff --git a/apps/cli/template/with-auth/apps/server/src/lib/with-express-context.ts b/apps/cli/template/with-auth/apps/server/src/lib/with-express-context.ts deleted file mode 100644 index 9b0a6d7..0000000 --- a/apps/cli/template/with-auth/apps/server/src/lib/with-express-context.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { CreateExpressContextOptions } from "@trpc/server/adapters/express"; -import { fromNodeHeaders } from "better-auth/node"; -import { auth } from "./auth"; - -export async function createContext(opts: CreateExpressContextOptions) { - const session = await auth.api.getSession({ - headers: fromNodeHeaders(opts.req.headers), - }); - return { - session, - }; -} - -export type Context = Awaited>; diff --git a/apps/cli/template/with-auth/apps/server/src/lib/with-hono-context.ts b/apps/cli/template/with-auth/apps/server/src/lib/with-hono-context.ts deleted file mode 100644 index 6eeee75..0000000 --- a/apps/cli/template/with-auth/apps/server/src/lib/with-hono-context.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { Context as HonoContext } from "hono"; -import { auth } from "./auth"; - -export type CreateContextOptions = { - context: HonoContext; -}; - -export async function createContext({ context }: CreateContextOptions) { - const session = await auth.api.getSession({ - headers: context.req.raw.headers, - }); - - return { - session, - }; -} - -export type Context = Awaited>; diff --git a/apps/cli/template/with-auth/apps/server/src/lib/with-next-context.ts b/apps/cli/template/with-auth/apps/server/src/lib/with-next-context.ts deleted file mode 100644 index 1dc3fc3..0000000 --- a/apps/cli/template/with-auth/apps/server/src/lib/with-next-context.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { NextRequest } from "next/server"; -import { auth } from "./auth"; - -export async function createContext(req: NextRequest) { - const session = await auth.api.getSession({ - headers: req.headers, - }); - - return { - session, - }; -} - -export type Context = Awaited>; diff --git a/apps/cli/template/with-auth/apps/server/src/routers/index.ts b/apps/cli/template/with-auth/apps/server/src/routers/index.ts deleted file mode 100644 index 4021d41..0000000 --- a/apps/cli/template/with-auth/apps/server/src/routers/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { protectedProcedure, publicProcedure, router } from "../lib/trpc"; -import { todoRouter } from "./todo"; - -export const appRouter = router({ - healthCheck: publicProcedure.query(() => { - return "OK"; - }), - privateData: protectedProcedure.query(({ ctx }) => { - return { - message: "This is private", - user: ctx.session.user, - }; - }), - todo: todoRouter, -}); - -export type AppRouter = typeof appRouter; diff --git a/apps/cli/template/with-auth/apps/server/src/with-drizzle-mysql-lib/auth.ts b/apps/cli/template/with-auth/apps/server/src/with-drizzle-mysql-lib/auth.ts deleted file mode 100644 index c1b8e59..0000000 --- a/apps/cli/template/with-auth/apps/server/src/with-drizzle-mysql-lib/auth.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { betterAuth } from "better-auth"; -import { drizzleAdapter } from "better-auth/adapters/drizzle"; -import { db } from "../db"; -import * as schema from "../db/schema/auth"; - -export const auth = betterAuth({ - database: drizzleAdapter(db, { - provider: "mysql", - schema: schema, - }), - trustedOrigins: [process.env.CORS_ORIGIN || ""], - emailAndPassword: { - enabled: true, - }, -}); diff --git a/apps/cli/template/with-auth/apps/server/src/with-drizzle-postgres-lib/auth.ts b/apps/cli/template/with-auth/apps/server/src/with-drizzle-postgres-lib/auth.ts deleted file mode 100644 index ed45465..0000000 --- a/apps/cli/template/with-auth/apps/server/src/with-drizzle-postgres-lib/auth.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { betterAuth } from "better-auth"; -import { drizzleAdapter } from "better-auth/adapters/drizzle"; -import { db } from "../db"; -import * as schema from "../db/schema/auth"; - -export const auth = betterAuth({ - database: drizzleAdapter(db, { - provider: "pg", - schema: schema, - }), - trustedOrigins: [process.env.CORS_ORIGIN || ""], - emailAndPassword: { - enabled: true, - }, -}); diff --git a/apps/cli/template/with-auth/apps/server/src/with-drizzle-sqlite-lib/auth.ts b/apps/cli/template/with-auth/apps/server/src/with-drizzle-sqlite-lib/auth.ts deleted file mode 100644 index 8b3bafc..0000000 --- a/apps/cli/template/with-auth/apps/server/src/with-drizzle-sqlite-lib/auth.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { betterAuth } from "better-auth"; -import { drizzleAdapter } from "better-auth/adapters/drizzle"; -import { db } from "../db"; -import * as schema from "../db/schema/auth"; - -export const auth = betterAuth({ - database: drizzleAdapter(db, { - provider: "sqlite", - schema: schema, - }), - trustedOrigins: [process.env.CORS_ORIGIN || ""], - emailAndPassword: { - enabled: true, - }, -}); diff --git a/apps/cli/template/with-auth/apps/server/src/with-express-index.ts b/apps/cli/template/with-auth/apps/server/src/with-express-index.ts deleted file mode 100644 index e6fb496..0000000 --- a/apps/cli/template/with-auth/apps/server/src/with-express-index.ts +++ /dev/null @@ -1,34 +0,0 @@ -import "dotenv/config"; -import { createExpressMiddleware } from "@trpc/server/adapters/express"; -import { toNodeHandler } from "better-auth/node"; -import cors from "cors"; -import express from "express"; -import { auth } from "./lib/auth"; -import { createContext } from "./lib/context"; -import { appRouter } from "./routers/index"; - -const app = express(); - -app.use( - cors({ - origin: process.env.CORS_ORIGIN || "", - methods: ["GET", "POST", "OPTIONS"], - allowedHeaders: ["Content-Type", "Authorization"], - credentials: true, - }), -); - -app.all("/api/auth{/*path}", toNodeHandler(auth)); -app.use(express.json()); - - -app.use("/trpc", createExpressMiddleware({ router: appRouter, createContext })); - - -app.get("/", (_req, res) => { - res.status(200).send("OK"); -}); - -app.listen(3000, () => { - console.log("Server is running on port 3000"); -}); diff --git a/apps/cli/template/with-auth/apps/server/src/with-hono-index.ts b/apps/cli/template/with-auth/apps/server/src/with-hono-index.ts deleted file mode 100644 index b941a35..0000000 --- a/apps/cli/template/with-auth/apps/server/src/with-hono-index.ts +++ /dev/null @@ -1,39 +0,0 @@ - -import { trpcServer } from "@hono/trpc-server"; -import "dotenv/config"; -import { Hono } from "hono"; -import { cors } from "hono/cors"; -import { logger } from "hono/logger"; -import { auth } from "./lib/auth"; -import { createContext } from "./lib/context"; -import { appRouter } from "./routers/index"; - -const app = new Hono(); - -app.use(logger()); - -app.use( - "/*", - cors({ - origin: process.env.CORS_ORIGIN || "", - allowMethods: ["GET", "POST", "OPTIONS"], - allowHeaders: ["Content-Type", "Authorization"], - credentials: true, - }), -); - -app.on(["POST", "GET"], "/api/auth/**", (c) => auth.handler(c.req.raw)); - -app.use( - "/trpc/*", - trpcServer({ - router: appRouter, - createContext: (_opts, context) => { - return createContext({ context }); - }, - }), -); - -app.get("/", (c) => { - return c.text("OK"); -}); diff --git a/apps/cli/template/with-auth/apps/server/src/with-prisma-mongodb-lib/auth.ts b/apps/cli/template/with-auth/apps/server/src/with-prisma-mongodb-lib/auth.ts deleted file mode 100644 index c966478..0000000 --- a/apps/cli/template/with-auth/apps/server/src/with-prisma-mongodb-lib/auth.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { betterAuth } from "better-auth"; -import { prismaAdapter } from "better-auth/adapters/prisma"; -import prisma from "../../prisma"; - -export const auth = betterAuth({ - database: prismaAdapter(prisma, { - provider: "mongodb", - }), - trustedOrigins: [process.env.CORS_ORIGIN || ""], - emailAndPassword: { enabled: true }, -}); diff --git a/apps/cli/template/with-auth/apps/server/src/with-prisma-mysql-lib/auth.ts b/apps/cli/template/with-auth/apps/server/src/with-prisma-mysql-lib/auth.ts deleted file mode 100644 index 896d8a0..0000000 --- a/apps/cli/template/with-auth/apps/server/src/with-prisma-mysql-lib/auth.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { betterAuth } from "better-auth"; -import { prismaAdapter } from "better-auth/adapters/prisma"; -import prisma from "../../prisma"; - -export const auth = betterAuth({ - database: prismaAdapter(prisma, { - provider: "mysql", - }), - trustedOrigins: [process.env.CORS_ORIGIN || ""], - emailAndPassword: { enabled: true }, -}); diff --git a/apps/cli/template/with-auth/apps/server/src/with-prisma-postgres-lib/auth.ts b/apps/cli/template/with-auth/apps/server/src/with-prisma-postgres-lib/auth.ts deleted file mode 100644 index 2e62bff..0000000 --- a/apps/cli/template/with-auth/apps/server/src/with-prisma-postgres-lib/auth.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { betterAuth } from "better-auth"; -import { prismaAdapter } from "better-auth/adapters/prisma"; -import prisma from "../../prisma"; - -export const auth = betterAuth({ - database: prismaAdapter(prisma, { - provider: "postgresql", - }), - trustedOrigins: [process.env.CORS_ORIGIN || ""], - emailAndPassword: { enabled: true }, -}); diff --git a/apps/cli/template/with-auth/apps/server/src/with-prisma-sqlite-lib/auth.ts b/apps/cli/template/with-auth/apps/server/src/with-prisma-sqlite-lib/auth.ts deleted file mode 100644 index c72c675..0000000 --- a/apps/cli/template/with-auth/apps/server/src/with-prisma-sqlite-lib/auth.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { betterAuth } from "better-auth"; -import { prismaAdapter } from "better-auth/adapters/prisma"; -import prisma from "../../prisma"; - -export const auth = betterAuth({ - database: prismaAdapter(prisma, { - provider: "sqlite", - }), - trustedOrigins: [process.env.CORS_ORIGIN || ""], - emailAndPassword: { enabled: true }, -}); diff --git a/apps/cli/template/with-auth/apps/web-base/src/lib/auth-client.ts b/apps/cli/template/with-auth/apps/web-base/src/lib/auth-client.ts deleted file mode 100644 index e91bf02..0000000 --- a/apps/cli/template/with-auth/apps/web-base/src/lib/auth-client.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { createAuthClient } from "better-auth/react"; - -export const authClient = createAuthClient({ - baseURL: import.meta.env.VITE_SERVER_URL, -}); diff --git a/apps/cli/template/with-auth/apps/web-next/src/components/header.tsx b/apps/cli/template/with-auth/apps/web-next/src/components/header.tsx deleted file mode 100644 index 2520f17..0000000 --- a/apps/cli/template/with-auth/apps/web-next/src/components/header.tsx +++ /dev/null @@ -1,33 +0,0 @@ -"use client" -import Link from "next/link"; -import { ModeToggle } from "./mode-toggle"; -import UserMenu from "./user-menu"; - -export default function Header() { - const links = [ - { to: "/", label: "Home" }, - { to: "/dashboard", label: "Dashboard" }, - ]; - - return ( -
-
- -
- - -
-
-
-
- ); -} diff --git a/apps/cli/template/with-auth/apps/web-next/src/lib/auth-client.ts b/apps/cli/template/with-auth/apps/web-next/src/lib/auth-client.ts deleted file mode 100644 index 34eb3c6..0000000 --- a/apps/cli/template/with-auth/apps/web-next/src/lib/auth-client.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { createAuthClient } from "better-auth/react"; - -export const authClient = createAuthClient({ - baseURL: process.env.NEXT_PUBLIC_SERVER_URL, -}); diff --git a/apps/cli/template/with-auth/apps/web-next/src/utils/trpc.ts b/apps/cli/template/with-auth/apps/web-next/src/utils/trpc.ts deleted file mode 100644 index 3e327a7..0000000 --- a/apps/cli/template/with-auth/apps/web-next/src/utils/trpc.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { QueryCache, QueryClient } from '@tanstack/react-query'; -import { createTRPCClient, httpBatchLink } from '@trpc/client'; -import { createTRPCOptionsProxy } from '@trpc/tanstack-react-query'; -import type { AppRouter } from '../../../server/src/routers'; -import { toast } from 'sonner'; - -export const queryClient = new QueryClient({ - queryCache: new QueryCache({ - onError: (error) => { - toast.error(error.message, { - action: { - label: "retry", - onClick: () => { - queryClient.invalidateQueries(); - }, - }, - }); - }, - }), -}); - -const trpcClient = createTRPCClient({ - links: [ - httpBatchLink({ - url: `${process.env.NEXT_PUBLIC_SERVER_URL}/trpc`, - fetch(url, options) { - return fetch(url, { - ...options, - credentials: "include", - }); - }, - }), - ], -}) - -export const trpc = createTRPCOptionsProxy({ - client: trpcClient, - queryClient, -}); diff --git a/apps/cli/template/with-auth/apps/web-react-router/src/components/header.tsx b/apps/cli/template/with-auth/apps/web-react-router/src/components/header.tsx deleted file mode 100644 index 6e0c10d..0000000 --- a/apps/cli/template/with-auth/apps/web-react-router/src/components/header.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { NavLink } from "react-router"; -import { ModeToggle } from "./mode-toggle"; -import UserMenu from "./user-menu"; - -export default function Header() { - const links = [ - { to: "/", label: "Home" }, - { to: "/dashboard", label: "Dashboard" }, - ]; - - return ( -
-
- -
- - -
-
-
-
- ); -} diff --git a/apps/cli/template/with-auth/apps/web-react-router/src/utils/trpc.ts b/apps/cli/template/with-auth/apps/web-react-router/src/utils/trpc.ts deleted file mode 100644 index 986941c..0000000 --- a/apps/cli/template/with-auth/apps/web-react-router/src/utils/trpc.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { AppRouter } from "../../../server/src/routers"; -import { QueryCache, QueryClient } from "@tanstack/react-query"; -import { createTRPCClient, httpBatchLink } from "@trpc/client"; -import { createTRPCOptionsProxy } from "@trpc/tanstack-react-query"; -import { toast } from "sonner"; - -export const queryClient = new QueryClient({ - queryCache: new QueryCache({ - onError: (error) => { - toast.error(error.message, { - action: { - label: "retry", - onClick: () => { - queryClient.invalidateQueries(); - }, - }, - }); - }, - }), -}); - -export const trpcClient = createTRPCClient({ - links: [ - httpBatchLink({ - url: `${import.meta.env.VITE_SERVER_URL}/trpc`, - fetch(url, options) { - return fetch(url, { - ...options, - credentials: "include", - }); - }, - }), - ], -}); - -export const trpc = createTRPCOptionsProxy({ - client: trpcClient, - queryClient, -}); diff --git a/apps/cli/template/with-auth/apps/web-tanstack-router/src/components/header.tsx b/apps/cli/template/with-auth/apps/web-tanstack-router/src/components/header.tsx deleted file mode 100644 index b3c4ef8..0000000 --- a/apps/cli/template/with-auth/apps/web-tanstack-router/src/components/header.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { Link } from "@tanstack/react-router"; -import { ModeToggle } from "./mode-toggle"; -import UserMenu from "./user-menu"; - -export default function Header() { - const links = [ - { to: "/", label: "Home" }, - { to: "/dashboard", label: "Dashboard" }, - ]; - - return ( -
-
- -
- - -
-
-
-
- ); -} diff --git a/apps/cli/template/with-auth/apps/web-tanstack-router/src/utils/trpc.ts b/apps/cli/template/with-auth/apps/web-tanstack-router/src/utils/trpc.ts deleted file mode 100644 index 986941c..0000000 --- a/apps/cli/template/with-auth/apps/web-tanstack-router/src/utils/trpc.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { AppRouter } from "../../../server/src/routers"; -import { QueryCache, QueryClient } from "@tanstack/react-query"; -import { createTRPCClient, httpBatchLink } from "@trpc/client"; -import { createTRPCOptionsProxy } from "@trpc/tanstack-react-query"; -import { toast } from "sonner"; - -export const queryClient = new QueryClient({ - queryCache: new QueryCache({ - onError: (error) => { - toast.error(error.message, { - action: { - label: "retry", - onClick: () => { - queryClient.invalidateQueries(); - }, - }, - }); - }, - }), -}); - -export const trpcClient = createTRPCClient({ - links: [ - httpBatchLink({ - url: `${import.meta.env.VITE_SERVER_URL}/trpc`, - fetch(url, options) { - return fetch(url, { - ...options, - credentials: "include", - }); - }, - }), - ], -}); - -export const trpc = createTRPCOptionsProxy({ - client: trpcClient, - queryClient, -}); diff --git a/apps/cli/template/with-auth/apps/web-tanstack-start/src/components/header.tsx b/apps/cli/template/with-auth/apps/web-tanstack-start/src/components/header.tsx deleted file mode 100644 index b30ea1b..0000000 --- a/apps/cli/template/with-auth/apps/web-tanstack-start/src/components/header.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { Link } from "@tanstack/react-router"; -import UserMenu from "./user-menu"; - -export default function Header() { - const links = [ - { to: "/", label: "Home" }, - { to: "/dashboard", label: "Dashboard" }, - ]; - - return ( -
-
- -
- -
-
-
-
- ); -} diff --git a/apps/cli/template/with-elysia/apps/server/src/index.ts b/apps/cli/template/with-elysia/apps/server/src/index.ts deleted file mode 100644 index a58e3fe..0000000 --- a/apps/cli/template/with-elysia/apps/server/src/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -import "dotenv/config"; -import { Elysia } from "elysia"; -import { cors } from "@elysiajs/cors"; -import { createContext } from "./lib/context"; -import { appRouter } from "./routers/index"; -import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; - -const app = new Elysia() - .use( - cors({ - origin: process.env.CORS_ORIGIN || "", - methods: ["GET", "POST", "OPTIONS"], - }), - ) - .all("/trpc/*", async (context) => { - const res = await fetchRequestHandler({ - endpoint: "/trpc", - router: appRouter, - req: context.request, - createContext: () => createContext({ context }), - }); - return res; - }) - .get("/", () => "OK") - .listen(3000, () => { - console.log(`Server is running on http://localhost:3000`); - }); diff --git a/apps/cli/template/with-elysia/apps/server/src/lib/context.ts b/apps/cli/template/with-elysia/apps/server/src/lib/context.ts deleted file mode 100644 index 6aaff80..0000000 --- a/apps/cli/template/with-elysia/apps/server/src/lib/context.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { Context as ElysiaContext } from "elysia"; - -export type CreateContextOptions = { - context: ElysiaContext; -}; - -export async function createContext({ context }: CreateContextOptions) { - return { - session: null, - }; -} - -export type Context = Awaited>; diff --git a/apps/cli/template/with-express/apps/server/src/index.ts b/apps/cli/template/with-express/apps/server/src/index.ts deleted file mode 100644 index 9a8f6f4..0000000 --- a/apps/cli/template/with-express/apps/server/src/index.ts +++ /dev/null @@ -1,29 +0,0 @@ -import "dotenv/config"; -import { createExpressMiddleware } from "@trpc/server/adapters/express"; -import cors from "cors"; -import express from "express"; -import { createContext } from "./lib/context"; -import { appRouter } from "./routers/index"; - -const app = express(); - -app.use( - cors({ - origin: process.env.CORS_ORIGIN || "", - methods: ["GET", "POST", "OPTIONS"], - allowedHeaders: ["Content-Type", "Authorization"], - credentials: true, - }), -); - -app.use(express.json()); - -app.use("/trpc", createExpressMiddleware({ router: appRouter, createContext })); - -app.get("/", (_req, res) => { - res.status(200).send("OK"); -}); - -app.listen(3000, () => { - console.log("Server is running on port 3000"); -}); diff --git a/apps/cli/template/with-express/apps/server/src/lib/context.ts b/apps/cli/template/with-express/apps/server/src/lib/context.ts deleted file mode 100644 index 389af2b..0000000 --- a/apps/cli/template/with-express/apps/server/src/lib/context.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { CreateExpressContextOptions } from "@trpc/server/adapters/express"; - -export async function createContext(opts: CreateExpressContextOptions) { - return { - session: null, - }; -} - -export type Context = Awaited>; diff --git a/apps/cli/template/with-hono/apps/server/src/index.ts b/apps/cli/template/with-hono/apps/server/src/index.ts deleted file mode 100644 index b5a2566..0000000 --- a/apps/cli/template/with-hono/apps/server/src/index.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { trpcServer } from "@hono/trpc-server"; -import "dotenv/config"; -import { Hono } from "hono"; -import { cors } from "hono/cors"; -import { logger } from "hono/logger"; -import { createContext } from "./lib/context"; -import { appRouter } from "./routers/index"; - -const app = new Hono(); - -app.use(logger()); - -app.use( - "/*", - cors({ - origin: process.env.CORS_ORIGIN || "", - allowMethods: ["GET", "POST", "OPTIONS"], - }), -); - -app.use( - "/trpc/*", - trpcServer({ - router: appRouter, - createContext: (_opts, context) => { - return createContext({ context }); - }, - }), -); - -app.get("/", (c) => { - return c.text("OK"); -}); diff --git a/apps/cli/template/with-hono/apps/server/src/lib/context.ts b/apps/cli/template/with-hono/apps/server/src/lib/context.ts deleted file mode 100644 index bbcb4b2..0000000 --- a/apps/cli/template/with-hono/apps/server/src/lib/context.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { Context as HonoContext } from "hono"; - -export type CreateContextOptions = { - context: HonoContext; -}; - -export async function createContext({ context }: CreateContextOptions) { - return { - session: null, - }; -} - -export type Context = Awaited>; diff --git a/apps/cli/template/with-next/apps/server/src/lib/context.ts b/apps/cli/template/with-next/apps/server/src/lib/context.ts deleted file mode 100644 index c29a034..0000000 --- a/apps/cli/template/with-next/apps/server/src/lib/context.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { NextRequest } from "next/server"; - -export async function createContext(req: NextRequest) { - return { - session: null, - }; -} - -export type Context = Awaited>; diff --git a/apps/cli/template/with-biome/biome.json b/apps/cli/templates/addons/biome/biome.json similarity index 100% rename from apps/cli/template/with-biome/biome.json rename to apps/cli/templates/addons/biome/biome.json diff --git a/apps/cli/template/with-husky/.husky/pre-commit b/apps/cli/templates/addons/husky/.husky/pre-commit similarity index 100% rename from apps/cli/template/with-husky/.husky/pre-commit rename to apps/cli/templates/addons/husky/.husky/pre-commit diff --git a/apps/cli/template/with-pwa/apps/web/public/logo.png b/apps/cli/templates/addons/pwa/apps/web/public/logo.png similarity index 100% rename from apps/cli/template/with-pwa/apps/web/public/logo.png rename to apps/cli/templates/addons/pwa/apps/web/public/logo.png diff --git a/apps/cli/template/with-pwa/apps/web/pwa-assets.config.ts b/apps/cli/templates/addons/pwa/apps/web/pwa-assets.config.ts similarity index 100% rename from apps/cli/template/with-pwa/apps/web/pwa-assets.config.ts rename to apps/cli/templates/addons/pwa/apps/web/pwa-assets.config.ts diff --git a/apps/cli/template/base/turbo.json b/apps/cli/templates/addons/turborepo/turbo.json similarity index 100% rename from apps/cli/template/base/turbo.json rename to apps/cli/templates/addons/turborepo/turbo.json diff --git a/apps/cli/templates/api/orpc/server/base/src/lib/context.ts.hbs b/apps/cli/templates/api/orpc/server/base/src/lib/context.ts.hbs new file mode 100644 index 0000000..ff1ab35 --- /dev/null +++ b/apps/cli/templates/api/orpc/server/base/src/lib/context.ts.hbs @@ -0,0 +1,105 @@ +{{#if (eq backend 'next')}} +import type { NextRequest } from "next/server"; +{{#if auth}} +import { auth } from "./auth"; +{{/if}} + +export async function createContext(req: NextRequest) { +{{#if auth}} + const session = await auth.api.getSession({ + headers: req.headers, + }); + return { + session, + }; +{{else}} + return {} +{{/if}} +} + +{{else if (eq backend 'hono')}} +import type { Context as HonoContext } from "hono"; +{{#if auth}} +import { auth } from "./auth"; +{{/if}} + +export type CreateContextOptions = { + context: HonoContext; +}; + +export async function createContext({ context }: CreateContextOptions) { +{{#if auth}} + const session = await auth.api.getSession({ + headers: context.req.raw.headers, + }); + return { + session, + }; +{{else}} + // No auth configured + return { + session: null, + }; +{{/if}} +} + +{{else if (eq backend 'elysia')}} +import type { Context as ElysiaContext } from "elysia"; +{{#if auth}} +import { auth } from "./auth"; +{{/if}} + +export type CreateContextOptions = { + context: ElysiaContext; +}; + +export async function createContext({ context }: CreateContextOptions) { +{{#if auth}} + const session = await auth.api.getSession({ + headers: context.request.headers, + }); + return { + session, + }; +{{else}} + // No auth configured + return { + session: null, + }; +{{/if}} +} + +{{else if (eq backend 'express')}} +import type { CreateExpressContextOptions } from "@trpc/server/adapters/express"; +{{#if auth}} +import { fromNodeHeaders } from "better-auth/node"; +import { auth } from "./auth"; +{{/if}} + +export async function createContext(opts: CreateExpressContextOptions) { +{{#if auth}} + const session = await auth.api.getSession({ + headers: fromNodeHeaders(opts.req.headers), + }); + return { + session, + }; +{{else}} + // No auth configured + return { + session: null, + }; +{{/if}} +} + +{{else}} +// Default or fallback context if backend is not recognized or none +// This might need adjustment based on your default behavior +export async function createContext() { + return { + session: null, + }; +} +{{/if}} + +export type Context = Awaited>; diff --git a/apps/cli/templates/api/orpc/server/base/src/lib/orpc.ts.hbs b/apps/cli/templates/api/orpc/server/base/src/lib/orpc.ts.hbs new file mode 100644 index 0000000..8bbd3a9 --- /dev/null +++ b/apps/cli/templates/api/orpc/server/base/src/lib/orpc.ts.hbs @@ -0,0 +1,17 @@ +import { ORPCError, os } from "@orpc/server"; +import type { Context } from "./context"; + +export const o = os.$context(); + +export const publicProcedure = o; + +{{#if auth}} +const requireAuth = o.middleware(async ({ context, next }) => { + if (!context.session?.user) { + throw new ORPCError("UNAUTHORIZED"); + } + return next({ context }); +}); + +export const protectedProcedure = publicProcedure.use(requireAuth); +{{/if}} diff --git a/apps/cli/templates/api/orpc/server/next/src/app/rpc/[...all]/route.ts.hbs b/apps/cli/templates/api/orpc/server/next/src/app/rpc/[...all]/route.ts.hbs new file mode 100644 index 0000000..94b5da5 --- /dev/null +++ b/apps/cli/templates/api/orpc/server/next/src/app/rpc/[...all]/route.ts.hbs @@ -0,0 +1,23 @@ +{{#if auth}} +import { createContext } from '@/lib/context' +{{/if}} +import { appRouter } from '@/routers' +import { RPCHandler } from '@orpc/server/fetch' +import { NextRequest } from 'next/server' + +const handler = new RPCHandler(appRouter) + +async function handleRequest(req: NextRequest) { + const { response } = await handler.handle(req, { + prefix: '/rpc', + context: {{#if auth}}await createContext(req){{else}}{}{{/if}}, + }) + + return response ?? new Response('Not found', { status: 404 }) +} + +export const GET = handleRequest +export const POST = handleRequest +export const PUT = handleRequest +export const PATCH = handleRequest +export const DELETE = handleRequest diff --git a/apps/cli/templates/api/orpc/web/base/src/utils/orpc.ts.hbs b/apps/cli/templates/api/orpc/web/base/src/utils/orpc.ts.hbs new file mode 100644 index 0000000..723607e --- /dev/null +++ b/apps/cli/templates/api/orpc/web/base/src/utils/orpc.ts.hbs @@ -0,0 +1,57 @@ +import { createORPCClient } from "@orpc/client"; +import { RPCLink } from "@orpc/client/fetch"; +import { createORPCReactQueryUtils } from "@orpc/react-query"; +import { QueryCache, QueryClient } from "@tanstack/react-query"; +import { toast } from "sonner"; +import type { appRouter } from "../../../server/src/routers/index"; +import type { RouterClient } from "@orpc/server"; +import { createContext, use } from 'react' +import type { RouterUtils } from '@orpc/react-query' + +type ORPCReactUtils = RouterUtils> + +export const queryClient = new QueryClient({ + queryCache: new QueryCache({ + onError: (error) => { + toast.error(`Error: ${error.message}`, { + action: { + label: "retry", + onClick: () => { + queryClient.invalidateQueries(); + }, + }, + }); + }, + }), +}); + +export const link = new RPCLink({ + {{#if (includes frontend "next")}} + url: `${process.env.NEXT_PUBLIC_SERVER_URL}/rpc`, + {{else}} + url: `${import.meta.env.VITE_SERVER_URL}/rpc`, + {{/if}} + {{#if auth}} + fetch(url, options) { + return fetch(url, { + ...options, + credentials: "include", + }); + }, + {{/if}} +}); + +export const client: RouterClient = createORPCClient(link) + +export const orpc = createORPCReactQueryUtils(client) + + +export const ORPCContext = createContext(undefined) + +export function useORPC(): ORPCReactUtils { + const orpc = use(ORPCContext) + if (!orpc) { + throw new Error('ORPCContext is not set up properly') + } + return orpc +} diff --git a/apps/cli/templates/api/trpc/server/base/src/lib/context.ts.hbs b/apps/cli/templates/api/trpc/server/base/src/lib/context.ts.hbs new file mode 100644 index 0000000..eb1cf8f --- /dev/null +++ b/apps/cli/templates/api/trpc/server/base/src/lib/context.ts.hbs @@ -0,0 +1,108 @@ +{{#if (eq backend 'next')}} +import type { NextRequest } from "next/server"; +{{#if auth}} +import { auth } from "./auth"; +{{/if}} + +export async function createContext(req: NextRequest) { +{{#if auth}} + const session = await auth.api.getSession({ + headers: req.headers, + }); + return { + session, + }; +{{else}} + // No auth configured + return { + session: null, + }; +{{/if}} +} + +{{else if (eq backend 'hono')}} +import type { Context as HonoContext } from "hono"; +{{#if auth}} +import { auth } from "./auth"; +{{/if}} + +export type CreateContextOptions = { + context: HonoContext; +}; + +export async function createContext({ context }: CreateContextOptions) { +{{#if auth}} + const session = await auth.api.getSession({ + headers: context.req.raw.headers, + }); + return { + session, + }; +{{else}} + // No auth configured + return { + session: null, + }; +{{/if}} +} + +{{else if (eq backend 'elysia')}} +import type { Context as ElysiaContext } from "elysia"; +{{#if auth}} +import { auth } from "./auth"; +{{/if}} + +export type CreateContextOptions = { + context: ElysiaContext; +}; + +export async function createContext({ context }: CreateContextOptions) { +{{#if auth}} + const session = await auth.api.getSession({ + headers: context.request.headers, + }); + return { + session, + }; +{{else}} + // No auth configured + return { + session: null, + }; +{{/if}} +} + +{{else if (eq backend 'express')}} +import type { CreateExpressContextOptions } from "@trpc/server/adapters/express"; +{{#if auth}} +import { fromNodeHeaders } from "better-auth/node"; +import { auth } from "./auth"; +{{/if}} + +export async function createContext(opts: CreateExpressContextOptions) { +{{#if auth}} + const session = await auth.api.getSession({ + headers: fromNodeHeaders(opts.req.headers), + }); + return { + session, + }; +{{else}} + // No auth configured + return { + session: null, + }; +{{/if}} +} + +{{else}} +// Default or fallback context if backend is not recognized or none +// This might need adjustment based on your default behavior +export async function createContext() { + return { + session: null, + }; +} +{{/if}} + +export type Context = Awaited>; diff --git a/apps/cli/template/with-auth/apps/server/src/lib/trpc.ts b/apps/cli/templates/api/trpc/server/base/src/lib/trpc.ts.hbs similarity index 96% rename from apps/cli/template/with-auth/apps/server/src/lib/trpc.ts rename to apps/cli/templates/api/trpc/server/base/src/lib/trpc.ts.hbs index 3affce2..f3dade3 100644 --- a/apps/cli/template/with-auth/apps/server/src/lib/trpc.ts +++ b/apps/cli/templates/api/trpc/server/base/src/lib/trpc.ts.hbs @@ -7,6 +7,7 @@ export const router = t.router; export const publicProcedure = t.procedure; +{{#if auth}} export const protectedProcedure = t.procedure.use(({ ctx, next }) => { if (!ctx.session) { throw new TRPCError({ @@ -22,3 +23,4 @@ export const protectedProcedure = t.procedure.use(({ ctx, next }) => { }, }); }); +{{/if}} diff --git a/apps/cli/template/with-next/apps/server/src/app/trpc/[trpc]/route.ts b/apps/cli/templates/api/trpc/server/next/src/app/trpc/[trpc]/route.ts similarity index 100% rename from apps/cli/template/with-next/apps/server/src/app/trpc/[trpc]/route.ts rename to apps/cli/templates/api/trpc/server/next/src/app/trpc/[trpc]/route.ts diff --git a/apps/cli/templates/api/trpc/web/base/src/utils/trpc.ts.hbs b/apps/cli/templates/api/trpc/web/base/src/utils/trpc.ts.hbs new file mode 100644 index 0000000..ba6dafe --- /dev/null +++ b/apps/cli/templates/api/trpc/web/base/src/utils/trpc.ts.hbs @@ -0,0 +1,100 @@ +{{#if (includes frontend 'next')}} +{{!-- Next.js tRPC Client Setup --}} +import { QueryCache, QueryClient } from '@tanstack/react-query'; +import { createTRPCClient, httpBatchLink } from '@trpc/client'; +import { createTRPCOptionsProxy } from '@trpc/tanstack-react-query'; +import type { AppRouter } from '../../../server/src/routers'; {{! Adjust path if necessary }} +import { toast } from 'sonner'; + +export const queryClient = new QueryClient({ + queryCache: new QueryCache({ + onError: (error) => { + toast.error(error.message, { + action: { + label: "retry", + onClick: () => { + queryClient.invalidateQueries(); + }, + }, + }); + }, + }), +}); + +const trpcClient = createTRPCClient({ + links: [ + httpBatchLink({ + {{#if (includes frontend 'next')}} + url: `${process.env.NEXT_PUBLIC_SERVER_URL}/trpc`, + {{else}} + url: `${import.meta.env.VITE_SERVER_URL}/trpc`, + {{/if}} + {{#if auth}} + fetch(url, options) { + return fetch(url, { + ...options, + credentials: "include", + }); + }, + {{/if}} + }), + ], +}) + +export const trpc = createTRPCOptionsProxy({ + client: trpcClient, + queryClient, +}); + +{{else if (includes frontend 'tanstack-start')}} +{{!-- TanStack Start tRPC Client Setup --}} +import { createTRPCContext } from "@trpc/tanstack-react-query"; +import type { AppRouter } from "../../../server/src/routers"; {{! Adjust path if necessary }} + +export const { TRPCProvider, useTRPC, useTRPCClient } = + createTRPCContext(); + +{{else}} +{{!-- Default Web tRPC Client Setup (TanStack Router, React Router, etc.) --}} +import type { AppRouter } from "../../../server/src/routers"; {{! Adjust path if necessary }} +import { QueryCache, QueryClient } from "@tanstack/react-query"; +import { createTRPCClient, httpBatchLink } from "@trpc/client"; +import { createTRPCOptionsProxy } from "@trpc/tanstack-react-query"; +import { toast } from "sonner"; + +export const queryClient = new QueryClient({ + queryCache: new QueryCache({ + onError: (error) => { + toast.error(error.message, { + action: { + label: "retry", + onClick: () => { + queryClient.invalidateQueries(); + }, + }, + }); + }, + }), +}); + +export const trpcClient = createTRPCClient({ + links: [ + httpBatchLink({ + url: `${import.meta.env.VITE_SERVER_URL}/trpc`, + {{#if auth}} + fetch(url, options) { + return fetch(url, { + ...options, + credentials: "include", + }); + }, + {{/if}} + }), + ], +}); + +export const trpc = createTRPCOptionsProxy({ + client: trpcClient, + queryClient, +}); +{{/if}} diff --git a/apps/cli/template/with-auth/apps/native/app/(drawer)/index.tsx b/apps/cli/templates/auth/native/app/(drawer)/index.tsx similarity index 100% rename from apps/cli/template/with-auth/apps/native/app/(drawer)/index.tsx rename to apps/cli/templates/auth/native/app/(drawer)/index.tsx diff --git a/apps/cli/template/with-auth/apps/native/components/sign-in.tsx b/apps/cli/templates/auth/native/components/sign-in.tsx similarity index 100% rename from apps/cli/template/with-auth/apps/native/components/sign-in.tsx rename to apps/cli/templates/auth/native/components/sign-in.tsx diff --git a/apps/cli/template/with-auth/apps/native/components/sign-up.tsx b/apps/cli/templates/auth/native/components/sign-up.tsx similarity index 100% rename from apps/cli/template/with-auth/apps/native/components/sign-up.tsx rename to apps/cli/templates/auth/native/components/sign-up.tsx diff --git a/apps/cli/template/with-auth/apps/native/lib/auth-client.ts b/apps/cli/templates/auth/native/lib/auth-client.ts similarity index 100% rename from apps/cli/template/with-auth/apps/native/lib/auth-client.ts rename to apps/cli/templates/auth/native/lib/auth-client.ts diff --git a/apps/cli/template/with-auth/apps/native/utils/trpc.ts b/apps/cli/templates/auth/native/utils/trpc.ts similarity index 100% rename from apps/cli/template/with-auth/apps/native/utils/trpc.ts rename to apps/cli/templates/auth/native/utils/trpc.ts diff --git a/apps/cli/templates/auth/server/base/src/lib/auth.ts.hbs b/apps/cli/templates/auth/server/base/src/lib/auth.ts.hbs new file mode 100644 index 0000000..c56ad52 --- /dev/null +++ b/apps/cli/templates/auth/server/base/src/lib/auth.ts.hbs @@ -0,0 +1,30 @@ +import { betterAuth } from "better-auth"; + +{{#if (eq orm "prisma")}} +import { prismaAdapter } from "better-auth/adapters/prisma"; +import prisma from "../../prisma"; + +export const auth = betterAuth({ + database: prismaAdapter(prisma, { + provider: "{{database}}" + }), + trustedOrigins: [process.env.CORS_ORIGIN || ""], + emailAndPassword: { enabled: true } +}); + +{{else if (eq orm "drizzle")}} +import { drizzleAdapter } from "better-auth/adapters/drizzle"; +import { db } from "../db"; +import * as schema from "../db/schema/auth"; + +export const auth = betterAuth({ + database: drizzleAdapter(db, { + {{#if (eq database "postgresql")}}provider: "pg",{{/if}} + {{#if (eq database "sqlite")}}provider: "sqlite",{{/if}} + {{#if (eq database "mysql")}}provider: "mysql",{{/if}} + schema: schema + }), + trustedOrigins: [process.env.CORS_ORIGIN || ""], + emailAndPassword: { enabled: true } +}); +{{/if}} \ No newline at end of file diff --git a/apps/cli/template/with-drizzle-mysql/apps/server/src/db/schema/auth.ts b/apps/cli/templates/auth/server/db/drizzle/mysql/src/db/schema/auth.ts similarity index 100% rename from apps/cli/template/with-drizzle-mysql/apps/server/src/db/schema/auth.ts rename to apps/cli/templates/auth/server/db/drizzle/mysql/src/db/schema/auth.ts diff --git a/apps/cli/template/with-drizzle-postgres/apps/server/src/db/schema/auth.ts b/apps/cli/templates/auth/server/db/drizzle/postgres/src/db/schema/auth.ts similarity index 100% rename from apps/cli/template/with-drizzle-postgres/apps/server/src/db/schema/auth.ts rename to apps/cli/templates/auth/server/db/drizzle/postgres/src/db/schema/auth.ts diff --git a/apps/cli/template/with-drizzle-sqlite/apps/server/src/db/schema/auth.ts b/apps/cli/templates/auth/server/db/drizzle/sqlite/src/db/schema/auth.ts similarity index 100% rename from apps/cli/template/with-drizzle-sqlite/apps/server/src/db/schema/auth.ts rename to apps/cli/templates/auth/server/db/drizzle/sqlite/src/db/schema/auth.ts diff --git a/apps/cli/template/with-prisma-sqlite/apps/server/prisma/schema/auth.prisma b/apps/cli/templates/auth/server/db/prisma/mongodb/prisma/schema/auth.prisma similarity index 100% rename from apps/cli/template/with-prisma-sqlite/apps/server/prisma/schema/auth.prisma rename to apps/cli/templates/auth/server/db/prisma/mongodb/prisma/schema/auth.prisma diff --git a/apps/cli/template/with-prisma-mysql/apps/server/prisma/schema/auth.prisma b/apps/cli/templates/auth/server/db/prisma/mysql/prisma/schema/auth.prisma similarity index 100% rename from apps/cli/template/with-prisma-mysql/apps/server/prisma/schema/auth.prisma rename to apps/cli/templates/auth/server/db/prisma/mysql/prisma/schema/auth.prisma diff --git a/apps/cli/template/with-prisma-postgres/apps/server/prisma/schema/auth.prisma b/apps/cli/templates/auth/server/db/prisma/postgres/prisma/schema/auth.prisma similarity index 100% rename from apps/cli/template/with-prisma-postgres/apps/server/prisma/schema/auth.prisma rename to apps/cli/templates/auth/server/db/prisma/postgres/prisma/schema/auth.prisma diff --git a/apps/cli/template/with-prisma-mongodb/apps/server/prisma/schema/auth.prisma b/apps/cli/templates/auth/server/db/prisma/sqlite/prisma/schema/auth.prisma similarity index 100% rename from apps/cli/template/with-prisma-mongodb/apps/server/prisma/schema/auth.prisma rename to apps/cli/templates/auth/server/db/prisma/sqlite/prisma/schema/auth.prisma diff --git a/apps/cli/template/with-auth/apps/server/src/with-next-app/api/auth/[...all]/route.ts b/apps/cli/templates/auth/server/next/src/app/api/auth/[...all]/route.ts similarity index 100% rename from apps/cli/template/with-auth/apps/server/src/with-next-app/api/auth/[...all]/route.ts rename to apps/cli/templates/auth/server/next/src/app/api/auth/[...all]/route.ts diff --git a/apps/cli/templates/auth/web/base/src/lib/auth-client.ts.hbs b/apps/cli/templates/auth/web/base/src/lib/auth-client.ts.hbs new file mode 100644 index 0000000..16fd3f1 --- /dev/null +++ b/apps/cli/templates/auth/web/base/src/lib/auth-client.ts.hbs @@ -0,0 +1,10 @@ +import { createAuthClient } from "better-auth/react"; + +export const authClient = createAuthClient({ + baseURL: + {{#if (includes frontend "next")}} + process.env.NEXT_PUBLIC_SERVER_URL, + {{else}} + import.meta.env.VITE_SERVER_URL, + {{/if}} +}); diff --git a/apps/cli/template/with-auth/apps/web-next/src/app/dashboard/page.tsx b/apps/cli/templates/auth/web/next/src/app/dashboard/page.tsx.hbs similarity index 76% rename from apps/cli/template/with-auth/apps/web-next/src/app/dashboard/page.tsx rename to apps/cli/templates/auth/web/next/src/app/dashboard/page.tsx.hbs index 939cb3c..2979c8e 100644 --- a/apps/cli/template/with-auth/apps/web-next/src/app/dashboard/page.tsx +++ b/apps/cli/templates/auth/web/next/src/app/dashboard/page.tsx.hbs @@ -1,6 +1,11 @@ "use client" import { authClient } from "@/lib/auth-client"; +{{#if (eq api "orpc")}} +import { orpc } from "@/utils/orpc"; +{{/if}} +{{#if (eq api "trpc")}} import { trpc } from "@/utils/trpc"; +{{/if}} import { useQuery } from "@tanstack/react-query"; import { useRouter } from "next/navigation"; import { useEffect } from "react"; @@ -9,7 +14,12 @@ export default function Dashboard() { const router = useRouter() const { data: session, isPending } = authClient.useSession(); + {{#if (eq api "orpc")}} + const privateData = useQuery(orpc.privateData.queryOptions()); + {{/if}} + {{#if (eq api "trpc")}} const privateData = useQuery(trpc.privateData.queryOptions()); + {{/if}} useEffect(() => { if (!session && !isPending) { diff --git a/apps/cli/template/with-auth/apps/web-next/src/app/login/page.tsx b/apps/cli/templates/auth/web/next/src/app/login/page.tsx similarity index 100% rename from apps/cli/template/with-auth/apps/web-next/src/app/login/page.tsx rename to apps/cli/templates/auth/web/next/src/app/login/page.tsx diff --git a/apps/cli/template/with-auth/apps/web-next/src/components/sign-in-form.tsx b/apps/cli/templates/auth/web/next/src/components/sign-in-form.tsx similarity index 100% rename from apps/cli/template/with-auth/apps/web-next/src/components/sign-in-form.tsx rename to apps/cli/templates/auth/web/next/src/components/sign-in-form.tsx diff --git a/apps/cli/template/with-auth/apps/web-next/src/components/sign-up-form.tsx b/apps/cli/templates/auth/web/next/src/components/sign-up-form.tsx similarity index 100% rename from apps/cli/template/with-auth/apps/web-next/src/components/sign-up-form.tsx rename to apps/cli/templates/auth/web/next/src/components/sign-up-form.tsx diff --git a/apps/cli/template/with-auth/apps/web-next/src/components/theme-provider.tsx b/apps/cli/templates/auth/web/next/src/components/theme-provider.tsx similarity index 100% rename from apps/cli/template/with-auth/apps/web-next/src/components/theme-provider.tsx rename to apps/cli/templates/auth/web/next/src/components/theme-provider.tsx diff --git a/apps/cli/template/with-auth/apps/web-next/src/components/user-menu.tsx b/apps/cli/templates/auth/web/next/src/components/user-menu.tsx similarity index 100% rename from apps/cli/template/with-auth/apps/web-next/src/components/user-menu.tsx rename to apps/cli/templates/auth/web/next/src/components/user-menu.tsx diff --git a/apps/cli/template/with-auth/apps/web-react-router/src/components/sign-in-form.tsx b/apps/cli/templates/auth/web/react-router/src/components/sign-in-form.tsx similarity index 100% rename from apps/cli/template/with-auth/apps/web-react-router/src/components/sign-in-form.tsx rename to apps/cli/templates/auth/web/react-router/src/components/sign-in-form.tsx diff --git a/apps/cli/template/with-auth/apps/web-react-router/src/components/sign-up-form.tsx b/apps/cli/templates/auth/web/react-router/src/components/sign-up-form.tsx similarity index 100% rename from apps/cli/template/with-auth/apps/web-react-router/src/components/sign-up-form.tsx rename to apps/cli/templates/auth/web/react-router/src/components/sign-up-form.tsx diff --git a/apps/cli/template/with-auth/apps/web-react-router/src/components/user-menu.tsx b/apps/cli/templates/auth/web/react-router/src/components/user-menu.tsx similarity index 100% rename from apps/cli/template/with-auth/apps/web-react-router/src/components/user-menu.tsx rename to apps/cli/templates/auth/web/react-router/src/components/user-menu.tsx diff --git a/apps/cli/template/with-auth/apps/web-react-router/src/routes/dashboard.tsx b/apps/cli/templates/auth/web/react-router/src/routes/dashboard.tsx.hbs similarity index 75% rename from apps/cli/template/with-auth/apps/web-react-router/src/routes/dashboard.tsx rename to apps/cli/templates/auth/web/react-router/src/routes/dashboard.tsx.hbs index b46efc1..80bbb8d 100644 --- a/apps/cli/template/with-auth/apps/web-react-router/src/routes/dashboard.tsx +++ b/apps/cli/templates/auth/web/react-router/src/routes/dashboard.tsx.hbs @@ -1,5 +1,10 @@ import { authClient } from "@/lib/auth-client"; +{{#if (eq api "orpc")}} +import { orpc } from "@/utils/orpc"; +{{/if}} +{{#if (eq api "trpc")}} import { trpc } from "@/utils/trpc"; +{{/if}} import { useQuery } from "@tanstack/react-query"; import { useEffect } from "react"; import { useNavigate } from "react-router"; @@ -8,7 +13,12 @@ export default function Dashboard() { const { data: session, isPending } = authClient.useSession(); const navigate = useNavigate(); + {{#if (eq api "orpc")}} + const privateData = useQuery(orpc.privateData.queryOptions()); + {{/if}} + {{#if (eq api "trpc")}} const privateData = useQuery(trpc.privateData.queryOptions()); + {{/if}} useEffect(() => { if (!session && !isPending) { diff --git a/apps/cli/template/with-auth/apps/web-react-router/src/routes/login.tsx b/apps/cli/templates/auth/web/react-router/src/routes/login.tsx similarity index 100% rename from apps/cli/template/with-auth/apps/web-react-router/src/routes/login.tsx rename to apps/cli/templates/auth/web/react-router/src/routes/login.tsx diff --git a/apps/cli/template/with-auth/apps/web-tanstack-start/src/components/sign-in-form.tsx b/apps/cli/templates/auth/web/tanstack-router/src/components/sign-in-form.tsx similarity index 100% rename from apps/cli/template/with-auth/apps/web-tanstack-start/src/components/sign-in-form.tsx rename to apps/cli/templates/auth/web/tanstack-router/src/components/sign-in-form.tsx diff --git a/apps/cli/template/with-auth/apps/web-tanstack-start/src/components/sign-up-form.tsx b/apps/cli/templates/auth/web/tanstack-router/src/components/sign-up-form.tsx similarity index 100% rename from apps/cli/template/with-auth/apps/web-tanstack-start/src/components/sign-up-form.tsx rename to apps/cli/templates/auth/web/tanstack-router/src/components/sign-up-form.tsx diff --git a/apps/cli/template/with-auth/apps/web-tanstack-start/src/components/user-menu.tsx b/apps/cli/templates/auth/web/tanstack-router/src/components/user-menu.tsx similarity index 100% rename from apps/cli/template/with-auth/apps/web-tanstack-start/src/components/user-menu.tsx rename to apps/cli/templates/auth/web/tanstack-router/src/components/user-menu.tsx diff --git a/apps/cli/template/with-auth/apps/web-tanstack-router/src/routes/dashboard.tsx b/apps/cli/templates/auth/web/tanstack-router/src/routes/dashboard.tsx.hbs similarity index 78% rename from apps/cli/template/with-auth/apps/web-tanstack-router/src/routes/dashboard.tsx rename to apps/cli/templates/auth/web/tanstack-router/src/routes/dashboard.tsx.hbs index c53ce0c..5f114f3 100644 --- a/apps/cli/template/with-auth/apps/web-tanstack-router/src/routes/dashboard.tsx +++ b/apps/cli/templates/auth/web/tanstack-router/src/routes/dashboard.tsx.hbs @@ -1,5 +1,10 @@ import { authClient } from "@/lib/auth-client"; +{{#if (eq api "orpc")}} +import { orpc } from "@/utils/orpc"; +{{/if}} +{{#if (eq api "trpc")}} import { trpc } from "@/utils/trpc"; +{{/if}} import { useQuery } from "@tanstack/react-query"; import { createFileRoute } from "@tanstack/react-router"; import { useEffect } from "react"; @@ -13,7 +18,12 @@ function RouteComponent() { const navigate = Route.useNavigate(); + {{#if (eq api "orpc")}} + const privateData = useQuery(orpc.privateData.queryOptions()); + {{/if}} + {{#if (eq api "trpc")}} const privateData = useQuery(trpc.privateData.queryOptions()); + {{/if}} useEffect(() => { if (!session && !isPending) { diff --git a/apps/cli/template/with-auth/apps/web-tanstack-start/src/routes/login.tsx b/apps/cli/templates/auth/web/tanstack-router/src/routes/login.tsx similarity index 100% rename from apps/cli/template/with-auth/apps/web-tanstack-start/src/routes/login.tsx rename to apps/cli/templates/auth/web/tanstack-router/src/routes/login.tsx diff --git a/apps/cli/template/with-auth/apps/web-tanstack-router/src/components/sign-in-form.tsx b/apps/cli/templates/auth/web/tanstack-start/src/components/sign-in-form.tsx similarity index 100% rename from apps/cli/template/with-auth/apps/web-tanstack-router/src/components/sign-in-form.tsx rename to apps/cli/templates/auth/web/tanstack-start/src/components/sign-in-form.tsx diff --git a/apps/cli/template/with-auth/apps/web-tanstack-router/src/components/sign-up-form.tsx b/apps/cli/templates/auth/web/tanstack-start/src/components/sign-up-form.tsx similarity index 100% rename from apps/cli/template/with-auth/apps/web-tanstack-router/src/components/sign-up-form.tsx rename to apps/cli/templates/auth/web/tanstack-start/src/components/sign-up-form.tsx diff --git a/apps/cli/template/with-auth/apps/web-tanstack-router/src/components/user-menu.tsx b/apps/cli/templates/auth/web/tanstack-start/src/components/user-menu.tsx similarity index 100% rename from apps/cli/template/with-auth/apps/web-tanstack-router/src/components/user-menu.tsx rename to apps/cli/templates/auth/web/tanstack-start/src/components/user-menu.tsx diff --git a/apps/cli/template/with-auth/apps/web-tanstack-start/src/routes/dashboard.tsx b/apps/cli/templates/auth/web/tanstack-start/src/routes/dashboard.tsx.hbs similarity index 72% rename from apps/cli/template/with-auth/apps/web-tanstack-start/src/routes/dashboard.tsx rename to apps/cli/templates/auth/web/tanstack-start/src/routes/dashboard.tsx.hbs index 0c7d689..c2f8ccf 100644 --- a/apps/cli/template/with-auth/apps/web-tanstack-start/src/routes/dashboard.tsx +++ b/apps/cli/templates/auth/web/tanstack-start/src/routes/dashboard.tsx.hbs @@ -1,5 +1,10 @@ import { authClient } from "@/lib/auth-client"; +{{#if (eq api "trpc")}} import { useTRPC } from "@/utils/trpc"; +{{/if}} +{{#if (eq api "orpc")}} +import { useORPC } from "@/utils/orpc"; +{{/if}} import { useQuery } from "@tanstack/react-query"; import { createFileRoute } from "@tanstack/react-router"; import { useEffect } from "react"; @@ -10,10 +15,20 @@ export const Route = createFileRoute("/dashboard")({ function RouteComponent() { const navigate = Route.useNavigate(); + {{#if (eq api "trpc")}} const trpc = useTRPC(); + {{/if}} + {{#if (eq api "orpc")}} + const orpc = useORPC(); + {{/if}} const { data: session, isPending } = authClient.useSession(); + {{#if (eq api "trpc")}} const privateData = useQuery(trpc.privateData.queryOptions()); + {{/if}} + {{#if (eq api "orpc")}} + const privateData = useQuery(orpc.privateData.queryOptions()); + {{/if}} useEffect(() => { if (!session && !isPending) { diff --git a/apps/cli/template/with-auth/apps/web-tanstack-router/src/routes/login.tsx b/apps/cli/templates/auth/web/tanstack-start/src/routes/login.tsx similarity index 100% rename from apps/cli/template/with-auth/apps/web-tanstack-router/src/routes/login.tsx rename to apps/cli/templates/auth/web/tanstack-start/src/routes/login.tsx diff --git a/apps/cli/template/with-auth/apps/server/src/with-elysia-index.ts b/apps/cli/templates/backend/elysia/src/index.ts.hbs similarity index 52% rename from apps/cli/template/with-auth/apps/server/src/with-elysia-index.ts rename to apps/cli/templates/backend/elysia/src/index.ts.hbs index 84577a1..498653a 100644 --- a/apps/cli/template/with-auth/apps/server/src/with-elysia-index.ts +++ b/apps/cli/templates/backend/elysia/src/index.ts.hbs @@ -1,20 +1,43 @@ +{{#if (eq runtime "node")}} +import { node } from "@elysiajs/node"; +{{/if}} import "dotenv/config"; import { Elysia } from "elysia"; import { cors } from "@elysiajs/cors"; -import { auth } from "./lib/auth"; +{{#if (eq api "trpc")}} import { createContext } from "./lib/context"; import { appRouter } from "./routers/index"; import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; +{{/if}} +{{#if (eq api "orpc")}} +import { RPCHandler } from "@orpc/server/fetch"; +import { appRouter } from "./routers"; +import { createContext } from "./lib/context"; +{{/if}} +{{#if auth}} +import { auth } from "./lib/auth"; +{{/if}} +{{#if (eq api "orpc")}} +const handler = new RPCHandler(appRouter); +{{/if}} + +{{#if (eq runtime "node")}} +const app = new Elysia({ adapter: node() }) +{{else}} const app = new Elysia() +{{/if}} .use( cors({ origin: process.env.CORS_ORIGIN || "", methods: ["GET", "POST", "OPTIONS"], + {{#if auth}} allowedHeaders: ["Content-Type", "Authorization"], credentials: true, + {{/if}} }), ) + {{#if auth}} .all("/api/auth/*", async (context) => { const { request } = context; if (["POST", "GET"].includes(request.method)) { @@ -22,6 +45,17 @@ const app = new Elysia() } context.error(405); }) + {{/if}} +{{#if (eq api "orpc")}} + .all('/rpc*', async (context) => { + const { response } = await handler.handle(context.request, { + prefix: '/rpc', + context: await createContext({ context }) + }) + return response ?? new Response('Not Found', { status: 404 }) + }) +{{/if}} +{{#if (eq api "trpc")}} .all("/trpc/*", async (context) => { const res = await fetchRequestHandler({ endpoint: "/trpc", @@ -31,7 +65,8 @@ const app = new Elysia() }); return res; }) +{{/if}} .get("/", () => "OK") .listen(3000, () => { - console.log("Server is running on http://localhost:3000"); - }); + console.log(`Server is running on http://localhost:3000`); + }); \ No newline at end of file diff --git a/apps/cli/templates/backend/express/src/index.ts.hbs b/apps/cli/templates/backend/express/src/index.ts.hbs new file mode 100644 index 0000000..d5fd975 --- /dev/null +++ b/apps/cli/templates/backend/express/src/index.ts.hbs @@ -0,0 +1,78 @@ +import "dotenv/config"; +{{#if (eq api "trpc")}} +import { createExpressMiddleware } from "@trpc/server/adapters/express"; +import { createContext } from "./lib/context"; +import { appRouter } from "./routers/index"; +{{/if}} +{{#if (eq api "orpc")}} +import { RPCHandler } from "@orpc/server/node"; +import { appRouter } from "./routers"; +{{/if}} +import cors from "cors"; +import express from "express"; +{{#if (includes examples "ai")}} +import { streamText } from "ai"; +import { google } from "@ai-sdk/google"; +{{/if}} +{{#if auth}} +import { auth } from "./lib/auth"; +{{/if}} + +const app = express(); + +app.use( + cors({ + origin: process.env.CORS_ORIGIN || "", + methods: ["GET", "POST", "OPTIONS"], + {{#if auth}} + allowedHeaders: ["Content-Type", "Authorization"], + credentials: true, + {{/if}} + }) +); + +{{#if auth}} +app.all("/api/auth{/*path}", toNodeHandler(auth)); +{{/if}} + +{{#if (eq api "trpc")}} +app.use( + "/trpc", + createExpressMiddleware({ + router: appRouter, + createContext + }) +); +{{/if}} + +{{#if (eq api "orpc")}} +const handler = new RPCHandler(appRouter); +app.use('/rpc{*path}', async (req, res, next) => { + const { matched } = await handler.handle(req, res, { + prefix: '/rpc', + context: {}, + }); + if (matched) return; + next(); +}); +{{/if}} + +{{#if (includes examples "ai")}} +// AI chat endpoint +app.post("/ai", async (req, res) => { + const { messages = [] } = req.body; + const result = streamText({ + model: google("gemini-1.5-flash"), + messages, + }); + result.pipeDataStreamToResponse(res); +}); +{{/if}} + +app.get("/", (_req, res) => { + res.status(200).send("OK"); +}); + +app.listen(3000, () => { + console.log("Server is running on port 3000"); +}); diff --git a/apps/cli/templates/backend/hono/src/index.ts.hbs b/apps/cli/templates/backend/hono/src/index.ts.hbs new file mode 100644 index 0000000..8519782 --- /dev/null +++ b/apps/cli/templates/backend/hono/src/index.ts.hbs @@ -0,0 +1,105 @@ +{{#if (eq api "orpc")}} +import { RPCHandler } from "@orpc/server/fetch"; +import { createContext } from "./lib/context"; +import { appRouter } from "./routers/index"; +{{#if auth}} +import { auth } from "./lib/auth"; +{{/if}} +{{/if}} +{{#if (eq api "trpc")}} +import { trpcServer } from "@hono/trpc-server"; +{{/if}} +import "dotenv/config"; +import { Hono } from "hono"; +import { cors } from "hono/cors"; +import { logger } from "hono/logger"; +{{#if (includes examples "ai")}} +import { streamText } from "ai"; +import { google } from "@ai-sdk/google"; +import { stream } from "hono/streaming"; +{{/if}} +{{#if (eq api "trpc")}} +import { createContext } from "./lib/context"; +import { appRouter } from "./routers/index"; +{{#if auth}} +import { auth } from "./lib/auth"; +{{/if}} +{{/if}} + +const app = new Hono(); + +app.use(logger()); +app.use( + "/*", + cors({ + origin: process.env.CORS_ORIGIN || "", + allowMethods: ["GET", "POST", "OPTIONS"], + {{#if auth}} + allowHeaders: ["Content-Type", "Authorization"], + credentials: true, + {{/if}} + }) +); + +{{#if auth}} +app.on(["POST", "GET"], "/api/auth/**", (c) => auth.handler(c.req.raw)); +{{/if}} + +{{#if (eq api "orpc")}} +const handler = new RPCHandler(appRouter); +app.use("/rpc/*", async (c, next) => { + const context = await createContext({ context: c }); + const { matched, response } = await handler.handle(c.req.raw, { + prefix: "/rpc", + context: context, + }); + if (matched) { + return c.newResponse(response.body, response); + } + await next(); +}); +{{/if}} + +{{#if (eq api "trpc")}} +app.use("/trpc/*", trpcServer({ + router: appRouter, + createContext: (_opts, context) => { + return createContext({ context }); + }, +})); +{{/if}} + +{{#if (includes examples "ai")}} +// AI chat endpoint +app.post("/ai", async (c) => { + const body = await c.req.json(); + const messages = body.messages || []; + + const result = streamText({ + model: google("gemini-1.5-flash"), + messages, + }); + + c.header("X-Vercel-AI-Data-Stream", "v1"); + c.header("Content-Type", "text/plain; charset=utf-8"); + + return stream(c, (stream) => stream.pipe(result.toDataStream())); +}); +{{/if}} + +app.get("/", (c) => { + return c.text("OK"); +}); + +{{#if (eq runtime "node")}} +import { serve } from "@hono/node-server"; + +serve({ + fetch: app.fetch, + port: 3000, +}, (info) => { + console.log(`Server is running on http://localhost:${info.port}`); +}); +{{else}} +export default app; +{{/if}} \ No newline at end of file diff --git a/apps/cli/template/with-next/apps/server/next-env.d.ts b/apps/cli/templates/backend/next/next-env.d.ts similarity index 100% rename from apps/cli/template/with-next/apps/server/next-env.d.ts rename to apps/cli/templates/backend/next/next-env.d.ts diff --git a/apps/cli/template/with-next/apps/server/next.config.ts b/apps/cli/templates/backend/next/next.config.ts similarity index 100% rename from apps/cli/template/with-next/apps/server/next.config.ts rename to apps/cli/templates/backend/next/next.config.ts diff --git a/apps/cli/template/with-next/apps/server/package.json b/apps/cli/templates/backend/next/package.json similarity index 83% rename from apps/cli/template/with-next/apps/server/package.json rename to apps/cli/templates/backend/next/package.json index 2395e3a..e39ed2e 100644 --- a/apps/cli/template/with-next/apps/server/package.json +++ b/apps/cli/templates/backend/next/package.json @@ -8,8 +8,6 @@ "start": "next start" }, "dependencies": { - "@trpc/client": "^11.1.0", - "@trpc/server": "^11.1.0", "next": "15.3.0" }, "devDependencies": { diff --git a/apps/cli/template/with-next/apps/server/src/app/route.ts b/apps/cli/templates/backend/next/src/app/route.ts similarity index 100% rename from apps/cli/template/with-next/apps/server/src/app/route.ts rename to apps/cli/templates/backend/next/src/app/route.ts diff --git a/apps/cli/template/with-next/apps/server/src/middleware.ts b/apps/cli/templates/backend/next/src/middleware.ts similarity index 100% rename from apps/cli/template/with-next/apps/server/src/middleware.ts rename to apps/cli/templates/backend/next/src/middleware.ts diff --git a/apps/cli/template/with-next/apps/server/tsconfig.json b/apps/cli/templates/backend/next/tsconfig.json similarity index 100% rename from apps/cli/template/with-next/apps/server/tsconfig.json rename to apps/cli/templates/backend/next/tsconfig.json diff --git a/apps/cli/template/base/apps/server/_gitignore b/apps/cli/templates/backend/server-base/_gitignore similarity index 100% rename from apps/cli/template/base/apps/server/_gitignore rename to apps/cli/templates/backend/server-base/_gitignore diff --git a/apps/cli/template/base/apps/server/package.json b/apps/cli/templates/backend/server-base/package.json similarity index 93% rename from apps/cli/template/base/apps/server/package.json rename to apps/cli/templates/backend/server-base/package.json index 8a79df8..06fdf77 100644 --- a/apps/cli/template/base/apps/server/package.json +++ b/apps/cli/templates/backend/server-base/package.json @@ -8,7 +8,6 @@ "compile": "bun build --compile --minify --sourcemap --bytecode ./src/index.ts --outfile server" }, "dependencies": { - "@trpc/server": "^11.0.0", "dotenv": "^16.4.7", "zod": "^3.24.2" }, diff --git a/apps/cli/templates/backend/server-base/src/routers/index.ts.hbs b/apps/cli/templates/backend/server-base/src/routers/index.ts.hbs new file mode 100644 index 0000000..80dfee4 --- /dev/null +++ b/apps/cli/templates/backend/server-base/src/routers/index.ts.hbs @@ -0,0 +1,51 @@ +{{#if (eq api "orpc")}} +import { {{#if auth}}protectedProcedure, {{/if}}publicProcedure } from "../lib/orpc"; +{{#if (includes examples "todo")}} +import { todoRouter } from "./todo"; +{{/if}} + +export const appRouter = { + healthCheck: publicProcedure.handler(() => { + return "OK"; + }), + {{#if auth}} + privateData: protectedProcedure.handler(({ context }) => { + return { + message: "This is private", + user: context.session!.user, + }; + }), + {{/if}} + {{#if (includes examples "todo")}} + todo: todoRouter, + {{/if}} +}; +{{/if}} + +{{#if (eq api "trpc")}} +import { + {{#if auth}}protectedProcedure, {{/if}}publicProcedure, + router, +} from "../lib/trpc"; +{{#if (includes examples "todo")}} +import { todoRouter } from "./todo"; +{{/if}} + +export const appRouter = router({ + healthCheck: publicProcedure.query(() => { + return "OK"; + }), + {{#if auth}} + privateData: protectedProcedure.query(({ ctx }) => { + return { + message: "This is private", + user: ctx.session.user, + }; + }), + {{/if}} + {{#if (includes examples "todo")}} + todo: todoRouter, + {{/if}} +}); +{{/if}} +export type AppRouter = typeof appRouter; \ No newline at end of file diff --git a/apps/cli/template/base/apps/server/tsconfig.json b/apps/cli/templates/backend/server-base/tsconfig.json similarity index 100% rename from apps/cli/template/base/apps/server/tsconfig.json rename to apps/cli/templates/backend/server-base/tsconfig.json diff --git a/apps/cli/template/base/_gitignore b/apps/cli/templates/base/_gitignore similarity index 100% rename from apps/cli/template/base/_gitignore rename to apps/cli/templates/base/_gitignore diff --git a/apps/cli/templates/base/package.json b/apps/cli/templates/base/package.json new file mode 100644 index 0000000..27e6d54 --- /dev/null +++ b/apps/cli/templates/base/package.json @@ -0,0 +1,10 @@ +{ + "name": "better-t-stack", + "private": true, + "workspaces": [ + "apps/*" + ], + "scripts": { + + } +} diff --git a/apps/cli/template/with-drizzle-mysql/apps/server/drizzle.config.ts b/apps/cli/templates/db/drizzle/mysql/drizzle.config.ts similarity index 100% rename from apps/cli/template/with-drizzle-mysql/apps/server/drizzle.config.ts rename to apps/cli/templates/db/drizzle/mysql/drizzle.config.ts diff --git a/apps/cli/template/with-drizzle-mysql/apps/server/src/db/index.ts b/apps/cli/templates/db/drizzle/mysql/src/db/index.ts similarity index 100% rename from apps/cli/template/with-drizzle-mysql/apps/server/src/db/index.ts rename to apps/cli/templates/db/drizzle/mysql/src/db/index.ts diff --git a/apps/cli/template/with-drizzle-postgres/apps/server/drizzle.config.ts b/apps/cli/templates/db/drizzle/postgres/drizzle.config.ts similarity index 100% rename from apps/cli/template/with-drizzle-postgres/apps/server/drizzle.config.ts rename to apps/cli/templates/db/drizzle/postgres/drizzle.config.ts diff --git a/apps/cli/template/with-drizzle-postgres/apps/server/src/db/index.ts b/apps/cli/templates/db/drizzle/postgres/src/db/index.ts similarity index 100% rename from apps/cli/template/with-drizzle-postgres/apps/server/src/db/index.ts rename to apps/cli/templates/db/drizzle/postgres/src/db/index.ts diff --git a/apps/cli/template/with-drizzle-sqlite/apps/server/drizzle.config.ts b/apps/cli/templates/db/drizzle/sqlite/drizzle.config.ts similarity index 100% rename from apps/cli/template/with-drizzle-sqlite/apps/server/drizzle.config.ts rename to apps/cli/templates/db/drizzle/sqlite/drizzle.config.ts diff --git a/apps/cli/template/with-drizzle-sqlite/apps/server/src/db/index.ts b/apps/cli/templates/db/drizzle/sqlite/src/db/index.ts similarity index 100% rename from apps/cli/template/with-drizzle-sqlite/apps/server/src/db/index.ts rename to apps/cli/templates/db/drizzle/sqlite/src/db/index.ts diff --git a/apps/cli/template/with-prisma-sqlite/apps/server/prisma/index.ts b/apps/cli/templates/db/prisma/mongodb/prisma/index.ts similarity index 100% rename from apps/cli/template/with-prisma-sqlite/apps/server/prisma/index.ts rename to apps/cli/templates/db/prisma/mongodb/prisma/index.ts diff --git a/apps/cli/template/with-prisma-mongodb/apps/server/prisma/schema/schema.prisma b/apps/cli/templates/db/prisma/mongodb/prisma/schema/schema.prisma similarity index 100% rename from apps/cli/template/with-prisma-mongodb/apps/server/prisma/schema/schema.prisma rename to apps/cli/templates/db/prisma/mongodb/prisma/schema/schema.prisma diff --git a/apps/cli/template/with-prisma-postgres/apps/server/prisma/index.ts b/apps/cli/templates/db/prisma/mysql/prisma/index.ts similarity index 100% rename from apps/cli/template/with-prisma-postgres/apps/server/prisma/index.ts rename to apps/cli/templates/db/prisma/mysql/prisma/index.ts diff --git a/apps/cli/template/with-prisma-mysql/apps/server/prisma/schema/schema.prisma b/apps/cli/templates/db/prisma/mysql/prisma/schema/schema.prisma similarity index 100% rename from apps/cli/template/with-prisma-mysql/apps/server/prisma/schema/schema.prisma rename to apps/cli/templates/db/prisma/mysql/prisma/schema/schema.prisma diff --git a/apps/cli/template/with-prisma-mysql/apps/server/prisma/index.ts b/apps/cli/templates/db/prisma/postgres/prisma/index.ts similarity index 100% rename from apps/cli/template/with-prisma-mysql/apps/server/prisma/index.ts rename to apps/cli/templates/db/prisma/postgres/prisma/index.ts diff --git a/apps/cli/template/with-prisma-postgres/apps/server/prisma/schema/schema.prisma b/apps/cli/templates/db/prisma/postgres/prisma/schema/schema.prisma similarity index 100% rename from apps/cli/template/with-prisma-postgres/apps/server/prisma/schema/schema.prisma rename to apps/cli/templates/db/prisma/postgres/prisma/schema/schema.prisma diff --git a/apps/cli/template/with-prisma-mongodb/apps/server/prisma/index.ts b/apps/cli/templates/db/prisma/sqlite/prisma/index.ts similarity index 100% rename from apps/cli/template/with-prisma-mongodb/apps/server/prisma/index.ts rename to apps/cli/templates/db/prisma/sqlite/prisma/index.ts diff --git a/apps/cli/template/with-prisma-sqlite/apps/server/prisma/schema/schema.prisma b/apps/cli/templates/db/prisma/sqlite/prisma/schema/schema.prisma similarity index 100% rename from apps/cli/template/with-prisma-sqlite/apps/server/prisma/schema/schema.prisma rename to apps/cli/templates/db/prisma/sqlite/prisma/schema/schema.prisma diff --git a/apps/cli/template/examples/ai/apps/web-react-router/src/routes/ai.tsx b/apps/cli/templates/examples/ai/apps/react-router/src/routes/ai.tsx similarity index 100% rename from apps/cli/template/examples/ai/apps/web-react-router/src/routes/ai.tsx rename to apps/cli/templates/examples/ai/apps/react-router/src/routes/ai.tsx diff --git a/apps/cli/template/examples/ai/apps/web-tanstack-start/src/routes/ai.tsx b/apps/cli/templates/examples/ai/apps/tanstack-router/src/routes/ai.tsx similarity index 100% rename from apps/cli/template/examples/ai/apps/web-tanstack-start/src/routes/ai.tsx rename to apps/cli/templates/examples/ai/apps/tanstack-router/src/routes/ai.tsx diff --git a/apps/cli/template/examples/ai/apps/web-tanstack-router/src/routes/ai.tsx b/apps/cli/templates/examples/ai/apps/tanstack-start/src/routes/ai.tsx similarity index 100% rename from apps/cli/template/examples/ai/apps/web-tanstack-router/src/routes/ai.tsx rename to apps/cli/templates/examples/ai/apps/tanstack-start/src/routes/ai.tsx diff --git a/apps/cli/templates/examples/todo/server/drizzle/base/src/routers/todo.ts.hbs b/apps/cli/templates/examples/todo/server/drizzle/base/src/routers/todo.ts.hbs new file mode 100644 index 0000000..8183c20 --- /dev/null +++ b/apps/cli/templates/examples/todo/server/drizzle/base/src/routers/todo.ts.hbs @@ -0,0 +1,79 @@ +{{#if (eq api "orpc")}} +import { eq } from "drizzle-orm"; +import { z } from "zod"; +import { db } from "../db"; +import { todo } from "../db/schema/todo"; +import { publicProcedure } from "../lib/orpc"; + +export const todoRouter = { + getAll: publicProcedure.handler(async () => { + return await db.select().from(todo); + }), + + create: publicProcedure + .input(z.object({ text: z.string().min(1) })) + .handler(async ({ input }) => { + const result = await db + .insert(todo) + .values({ + text: input.text, + }) + .returning(); + return result[0]; + }), + + toggle: publicProcedure + .input(z.object({ id: z.number(), completed: z.boolean() })) + .handler(async ({ input }) => { + await db + .update(todo) + .set({ completed: input.completed }) + .where(eq(todo.id, input.id)); + return { success: true }; + }), + + delete: publicProcedure + .input(z.object({ id: z.number() })) + .handler(async ({ input }) => { + await db.delete(todo).where(eq(todo.id, input.id)); + return { success: true }; + }), +}; +{{/if}} + +{{#if (eq api "trpc")}} +import { z } from "zod"; +import { router, publicProcedure } from "../lib/trpc"; +import { todo } from "../db/schema/todo"; +import { eq } from "drizzle-orm"; +import { db } from "../db"; + +export const todoRouter = router({ + getAll: publicProcedure.query(async () => { + return await db.select().from(todo); + }), + + create: publicProcedure + .input(z.object({ text: z.string().min(1) })) + .mutation(async ({ input }) => { + return await db.insert(todo).values({ + text: input.text, + }); + }), + + toggle: publicProcedure + .input(z.object({ id: z.number(), completed: z.boolean() })) + .mutation(async ({ input }) => { + return await db + .update(todo) + .set({ completed: input.completed }) + .where(eq(todo.id, input.id)); + }), + + delete: publicProcedure + .input(z.object({ id: z.number() })) + .mutation(async ({ input }) => { + return await db.delete(todo).where(eq(todo.id, input.id)); + }), +}); +{{/if}} diff --git a/apps/cli/template/with-drizzle-mysql/apps/server/src/db/schema/todo.ts b/apps/cli/templates/examples/todo/server/drizzle/mysql/src/db/schema/todo.ts similarity index 100% rename from apps/cli/template/with-drizzle-mysql/apps/server/src/db/schema/todo.ts rename to apps/cli/templates/examples/todo/server/drizzle/mysql/src/db/schema/todo.ts diff --git a/apps/cli/template/with-drizzle-postgres/apps/server/src/db/schema/todo.ts b/apps/cli/templates/examples/todo/server/drizzle/postgres/src/db/schema/todo.ts similarity index 100% rename from apps/cli/template/with-drizzle-postgres/apps/server/src/db/schema/todo.ts rename to apps/cli/templates/examples/todo/server/drizzle/postgres/src/db/schema/todo.ts diff --git a/apps/cli/template/with-drizzle-sqlite/apps/server/src/db/schema/todo.ts b/apps/cli/templates/examples/todo/server/drizzle/sqlite/src/db/schema/todo.ts similarity index 71% rename from apps/cli/template/with-drizzle-sqlite/apps/server/src/db/schema/todo.ts rename to apps/cli/templates/examples/todo/server/drizzle/sqlite/src/db/schema/todo.ts index 710fd27..de95bbf 100644 --- a/apps/cli/template/with-drizzle-sqlite/apps/server/src/db/schema/todo.ts +++ b/apps/cli/templates/examples/todo/server/drizzle/sqlite/src/db/schema/todo.ts @@ -1,7 +1,7 @@ -import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core"; +import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"; export const todo = sqliteTable("todo", { id: integer("id").primaryKey({ autoIncrement: true }), text: text("text").notNull(), - completed: integer("completed", { mode: "boolean" }).default(false).notNull() + completed: integer("completed", { mode: "boolean" }).default(false).notNull(), }); diff --git a/apps/cli/template/examples/todo/apps/server/src/routers/with-prisma-todo.ts b/apps/cli/templates/examples/todo/server/prisma/base/trpc/src/routers/todo.ts similarity index 100% rename from apps/cli/template/examples/todo/apps/server/src/routers/with-prisma-todo.ts rename to apps/cli/templates/examples/todo/server/prisma/base/trpc/src/routers/todo.ts diff --git a/apps/cli/template/with-prisma-mongodb/apps/server/prisma/schema/todo.prisma b/apps/cli/templates/examples/todo/server/prisma/mongodb/prisma/schema/todo.prisma similarity index 100% rename from apps/cli/template/with-prisma-mongodb/apps/server/prisma/schema/todo.prisma rename to apps/cli/templates/examples/todo/server/prisma/mongodb/prisma/schema/todo.prisma diff --git a/apps/cli/template/with-prisma-sqlite/apps/server/prisma/schema/todo.prisma b/apps/cli/templates/examples/todo/server/prisma/mysql/prisma/schema/todo.prisma similarity index 100% rename from apps/cli/template/with-prisma-sqlite/apps/server/prisma/schema/todo.prisma rename to apps/cli/templates/examples/todo/server/prisma/mysql/prisma/schema/todo.prisma diff --git a/apps/cli/template/with-prisma-postgres/apps/server/prisma/schema/todo.prisma b/apps/cli/templates/examples/todo/server/prisma/postgres/prisma/schema/todo.prisma similarity index 100% rename from apps/cli/template/with-prisma-postgres/apps/server/prisma/schema/todo.prisma rename to apps/cli/templates/examples/todo/server/prisma/postgres/prisma/schema/todo.prisma diff --git a/apps/cli/template/with-prisma-mysql/apps/server/prisma/schema/todo.prisma b/apps/cli/templates/examples/todo/server/prisma/sqlite/prisma/schema/todo.prisma similarity index 100% rename from apps/cli/template/with-prisma-mysql/apps/server/prisma/schema/todo.prisma rename to apps/cli/templates/examples/todo/server/prisma/sqlite/prisma/schema/todo.prisma diff --git a/apps/cli/template/examples/todo/apps/web-react-router/src/routes/todos.tsx b/apps/cli/templates/examples/todo/web/react-router/src/routes/todos.tsx.hbs similarity index 85% rename from apps/cli/template/examples/todo/apps/web-react-router/src/routes/todos.tsx rename to apps/cli/templates/examples/todo/web/react-router/src/routes/todos.tsx.hbs index d1b22e9..99c383f 100644 --- a/apps/cli/template/examples/todo/apps/web-react-router/src/routes/todos.tsx +++ b/apps/cli/templates/examples/todo/web/react-router/src/routes/todos.tsx.hbs @@ -8,7 +8,12 @@ import { } from "@/components/ui/card"; import { Checkbox } from "@/components/ui/checkbox"; import { Input } from "@/components/ui/input"; +{{#if (eq api "orpc")}} +import { orpc } from "@/utils/orpc"; +{{/if}} +{{#if (eq api "trpc")}} import { trpc } from "@/utils/trpc"; +{{/if}} import { useMutation, useQuery } from "@tanstack/react-query"; import { Loader2, Trash2 } from "lucide-react"; import { useState } from "react"; @@ -16,6 +21,28 @@ import { useState } from "react"; export default function Todos() { const [newTodoText, setNewTodoText] = useState(""); + {{#if (eq api "orpc")}} + const todos = useQuery(orpc.todo.getAll.queryOptions()); + const createMutation = useMutation( + orpc.todo.create.mutationOptions({ + onSuccess: () => { + todos.refetch(); + setNewTodoText(""); + }, + }) + ); + const toggleMutation = useMutation( + orpc.todo.toggle.mutationOptions({ + onSuccess: () => todos.refetch(), + }) + ); + const deleteMutation = useMutation( + orpc.todo.delete.mutationOptions({ + onSuccess: () => todos.refetch(), + }) + ); + {{/if}} + {{#if (eq api "trpc")}} const todos = useQuery(trpc.todo.getAll.queryOptions()); const createMutation = useMutation( trpc.todo.create.mutationOptions({ @@ -35,6 +62,7 @@ export default function Todos() { onSuccess: () => todos.refetch(), }) ); + {{/if}} const handleAddTodo = (e: React.FormEvent) => { e.preventDefault(); diff --git a/apps/cli/template/examples/todo/apps/web-tanstack-router/src/routes/todos.tsx b/apps/cli/templates/examples/todo/web/tanstack-router/src/routes/todos.tsx.hbs similarity index 85% rename from apps/cli/template/examples/todo/apps/web-tanstack-router/src/routes/todos.tsx rename to apps/cli/templates/examples/todo/web/tanstack-router/src/routes/todos.tsx.hbs index 8265414..9763948 100644 --- a/apps/cli/template/examples/todo/apps/web-tanstack-router/src/routes/todos.tsx +++ b/apps/cli/templates/examples/todo/web/tanstack-router/src/routes/todos.tsx.hbs @@ -8,7 +8,12 @@ import { } from "@/components/ui/card"; import { Checkbox } from "@/components/ui/checkbox"; import { Input } from "@/components/ui/input"; +{{#if (eq api "orpc")}} +import { orpc } from "@/utils/orpc"; +{{/if}} +{{#if (eq api "trpc")}} import { trpc } from "@/utils/trpc"; +{{/if}} import { useMutation, useQuery } from "@tanstack/react-query"; import { createFileRoute } from "@tanstack/react-router"; import { Loader2, Trash2 } from "lucide-react"; @@ -21,6 +26,28 @@ export const Route = createFileRoute("/todos")({ function TodosRoute() { const [newTodoText, setNewTodoText] = useState(""); + {{#if (eq api "orpc")}} + const todos = useQuery(orpc.todo.getAll.queryOptions()); + const createMutation = useMutation( + orpc.todo.create.mutationOptions({ + onSuccess: () => { + todos.refetch(); + setNewTodoText(""); + }, + }), + ); + const toggleMutation = useMutation( + orpc.todo.toggle.mutationOptions({ + onSuccess: () => todos.refetch(), + }), + ); + const deleteMutation = useMutation( + orpc.todo.delete.mutationOptions({ + onSuccess: () => todos.refetch(), + }), + ); + {{/if}} + {{#if (eq api "trpc")}} const todos = useQuery(trpc.todo.getAll.queryOptions()); const createMutation = useMutation( trpc.todo.create.mutationOptions({ @@ -40,6 +67,7 @@ function TodosRoute() { onSuccess: () => todos.refetch(), }), ); + {{/if}} const handleAddTodo = (e: React.FormEvent) => { e.preventDefault(); diff --git a/apps/cli/template/examples/todo/apps/web-tanstack-start/src/routes/todos.tsx b/apps/cli/templates/examples/todo/web/tanstack-start/src/routes/todos.tsx.hbs similarity index 84% rename from apps/cli/template/examples/todo/apps/web-tanstack-start/src/routes/todos.tsx rename to apps/cli/templates/examples/todo/web/tanstack-start/src/routes/todos.tsx.hbs index a9c3414..b8ca2bd 100644 --- a/apps/cli/template/examples/todo/apps/web-tanstack-start/src/routes/todos.tsx +++ b/apps/cli/templates/examples/todo/web/tanstack-start/src/routes/todos.tsx.hbs @@ -8,7 +8,12 @@ import { } from "@/components/ui/card"; import { Checkbox } from "@/components/ui/checkbox"; import { Input } from "@/components/ui/input"; +{{#if (eq api "trpc")}} import { useTRPC } from "@/utils/trpc"; +{{/if}} +{{#if (eq api "orpc")}} +import { useORPC } from "@/utils/orpc"; +{{/if}} import { useMutation, useQuery } from "@tanstack/react-query"; import { createFileRoute } from "@tanstack/react-router"; import { Loader2, Trash2 } from "lucide-react"; @@ -19,10 +24,16 @@ export const Route = createFileRoute("/todos")({ }); function TodosRoute() { + {{#if (eq api "trpc")}} const trpc = useTRPC(); + {{/if}} + {{#if (eq api "orpc")}} + const orpc = useORPC(); + {{/if}} const [newTodoText, setNewTodoText] = useState(""); + {{#if (eq api "trpc")}} const todos = useQuery(trpc.todo.getAll.queryOptions()); const createMutation = useMutation( trpc.todo.create.mutationOptions({ @@ -42,6 +53,28 @@ function TodosRoute() { onSuccess: () => todos.refetch(), }), ); + {{/if}} + {{#if (eq api "orpc")}} + const todos = useQuery(orpc.todo.getAll.queryOptions()); + const createMutation = useMutation( + orpc.todo.create.mutationOptions({ + onSuccess: () => { + todos.refetch(); + setNewTodoText(""); + }, + }), + ); + const toggleMutation = useMutation( + orpc.todo.toggle.mutationOptions({ + onSuccess: () => todos.refetch(), + }), + ); + const deleteMutation = useMutation( + orpc.todo.delete.mutationOptions({ + onSuccess: () => todos.refetch(), + }), + ); + {{/if}} const handleAddTodo = (e: React.FormEvent) => { e.preventDefault(); diff --git a/apps/cli/templates/extras/.npmrc b/apps/cli/templates/extras/.npmrc new file mode 100644 index 0000000..d67f374 --- /dev/null +++ b/apps/cli/templates/extras/.npmrc @@ -0,0 +1 @@ +node-linker=hoisted diff --git a/apps/cli/template/with-pnpm/pnpm-workspace.yaml b/apps/cli/templates/extras/pnpm-workspace.yaml similarity index 100% rename from apps/cli/template/with-pnpm/pnpm-workspace.yaml rename to apps/cli/templates/extras/pnpm-workspace.yaml diff --git a/apps/cli/template/base/apps/native/_gitignore b/apps/cli/templates/frontend/native/_gitignore similarity index 100% rename from apps/cli/template/base/apps/native/_gitignore rename to apps/cli/templates/frontend/native/_gitignore diff --git a/apps/cli/template/base/apps/native/app-env.d.ts b/apps/cli/templates/frontend/native/app-env.d.ts similarity index 100% rename from apps/cli/template/base/apps/native/app-env.d.ts rename to apps/cli/templates/frontend/native/app-env.d.ts diff --git a/apps/cli/template/base/apps/native/app.json b/apps/cli/templates/frontend/native/app.json similarity index 100% rename from apps/cli/template/base/apps/native/app.json rename to apps/cli/templates/frontend/native/app.json diff --git a/apps/cli/template/base/apps/native/app/(drawer)/(tabs)/_layout.tsx b/apps/cli/templates/frontend/native/app/(drawer)/(tabs)/_layout.tsx similarity index 100% rename from apps/cli/template/base/apps/native/app/(drawer)/(tabs)/_layout.tsx rename to apps/cli/templates/frontend/native/app/(drawer)/(tabs)/_layout.tsx diff --git a/apps/cli/template/base/apps/native/app/(drawer)/(tabs)/index.tsx b/apps/cli/templates/frontend/native/app/(drawer)/(tabs)/index.tsx similarity index 100% rename from apps/cli/template/base/apps/native/app/(drawer)/(tabs)/index.tsx rename to apps/cli/templates/frontend/native/app/(drawer)/(tabs)/index.tsx diff --git a/apps/cli/template/base/apps/native/app/(drawer)/(tabs)/two.tsx b/apps/cli/templates/frontend/native/app/(drawer)/(tabs)/two.tsx similarity index 100% rename from apps/cli/template/base/apps/native/app/(drawer)/(tabs)/two.tsx rename to apps/cli/templates/frontend/native/app/(drawer)/(tabs)/two.tsx diff --git a/apps/cli/template/base/apps/native/app/(drawer)/_layout.tsx b/apps/cli/templates/frontend/native/app/(drawer)/_layout.tsx similarity index 100% rename from apps/cli/template/base/apps/native/app/(drawer)/_layout.tsx rename to apps/cli/templates/frontend/native/app/(drawer)/_layout.tsx diff --git a/apps/cli/template/base/apps/native/app/(drawer)/index.tsx b/apps/cli/templates/frontend/native/app/(drawer)/index.tsx similarity index 100% rename from apps/cli/template/base/apps/native/app/(drawer)/index.tsx rename to apps/cli/templates/frontend/native/app/(drawer)/index.tsx diff --git a/apps/cli/template/base/apps/native/app/+html.tsx b/apps/cli/templates/frontend/native/app/+html.tsx similarity index 100% rename from apps/cli/template/base/apps/native/app/+html.tsx rename to apps/cli/templates/frontend/native/app/+html.tsx diff --git a/apps/cli/template/base/apps/native/app/+not-found.tsx b/apps/cli/templates/frontend/native/app/+not-found.tsx similarity index 100% rename from apps/cli/template/base/apps/native/app/+not-found.tsx rename to apps/cli/templates/frontend/native/app/+not-found.tsx diff --git a/apps/cli/template/base/apps/native/app/_layout.tsx b/apps/cli/templates/frontend/native/app/_layout.tsx similarity index 100% rename from apps/cli/template/base/apps/native/app/_layout.tsx rename to apps/cli/templates/frontend/native/app/_layout.tsx diff --git a/apps/cli/template/base/apps/native/app/modal.tsx b/apps/cli/templates/frontend/native/app/modal.tsx similarity index 100% rename from apps/cli/template/base/apps/native/app/modal.tsx rename to apps/cli/templates/frontend/native/app/modal.tsx diff --git a/apps/cli/template/base/apps/native/assets/adaptive-icon.png b/apps/cli/templates/frontend/native/assets/adaptive-icon.png similarity index 100% rename from apps/cli/template/base/apps/native/assets/adaptive-icon.png rename to apps/cli/templates/frontend/native/assets/adaptive-icon.png diff --git a/apps/cli/template/base/apps/native/assets/favicon.png b/apps/cli/templates/frontend/native/assets/favicon.png similarity index 100% rename from apps/cli/template/base/apps/native/assets/favicon.png rename to apps/cli/templates/frontend/native/assets/favicon.png diff --git a/apps/cli/template/base/apps/native/assets/icon.png b/apps/cli/templates/frontend/native/assets/icon.png similarity index 100% rename from apps/cli/template/base/apps/native/assets/icon.png rename to apps/cli/templates/frontend/native/assets/icon.png diff --git a/apps/cli/template/base/apps/native/assets/splash.png b/apps/cli/templates/frontend/native/assets/splash.png similarity index 100% rename from apps/cli/template/base/apps/native/assets/splash.png rename to apps/cli/templates/frontend/native/assets/splash.png diff --git a/apps/cli/template/base/apps/native/babel.config.js b/apps/cli/templates/frontend/native/babel.config.js similarity index 100% rename from apps/cli/template/base/apps/native/babel.config.js rename to apps/cli/templates/frontend/native/babel.config.js diff --git a/apps/cli/template/base/apps/native/components/container.tsx b/apps/cli/templates/frontend/native/components/container.tsx similarity index 100% rename from apps/cli/template/base/apps/native/components/container.tsx rename to apps/cli/templates/frontend/native/components/container.tsx diff --git a/apps/cli/template/base/apps/native/components/header-button.tsx b/apps/cli/templates/frontend/native/components/header-button.tsx similarity index 100% rename from apps/cli/template/base/apps/native/components/header-button.tsx rename to apps/cli/templates/frontend/native/components/header-button.tsx diff --git a/apps/cli/template/base/apps/native/components/tabbar-icon.tsx b/apps/cli/templates/frontend/native/components/tabbar-icon.tsx similarity index 100% rename from apps/cli/template/base/apps/native/components/tabbar-icon.tsx rename to apps/cli/templates/frontend/native/components/tabbar-icon.tsx diff --git a/apps/cli/template/base/apps/native/global.css b/apps/cli/templates/frontend/native/global.css similarity index 100% rename from apps/cli/template/base/apps/native/global.css rename to apps/cli/templates/frontend/native/global.css diff --git a/apps/cli/template/base/apps/native/lib/android-navigation-bar.tsx b/apps/cli/templates/frontend/native/lib/android-navigation-bar.tsx similarity index 100% rename from apps/cli/template/base/apps/native/lib/android-navigation-bar.tsx rename to apps/cli/templates/frontend/native/lib/android-navigation-bar.tsx diff --git a/apps/cli/template/base/apps/native/lib/constants.ts b/apps/cli/templates/frontend/native/lib/constants.ts similarity index 100% rename from apps/cli/template/base/apps/native/lib/constants.ts rename to apps/cli/templates/frontend/native/lib/constants.ts diff --git a/apps/cli/template/base/apps/native/lib/use-color-scheme.ts b/apps/cli/templates/frontend/native/lib/use-color-scheme.ts similarity index 100% rename from apps/cli/template/base/apps/native/lib/use-color-scheme.ts rename to apps/cli/templates/frontend/native/lib/use-color-scheme.ts diff --git a/apps/cli/template/base/apps/native/metro.config.js b/apps/cli/templates/frontend/native/metro.config.js similarity index 100% rename from apps/cli/template/base/apps/native/metro.config.js rename to apps/cli/templates/frontend/native/metro.config.js diff --git a/apps/cli/template/base/apps/native/package.json b/apps/cli/templates/frontend/native/package.json similarity index 100% rename from apps/cli/template/base/apps/native/package.json rename to apps/cli/templates/frontend/native/package.json diff --git a/apps/cli/template/base/apps/native/tailwind.config.js b/apps/cli/templates/frontend/native/tailwind.config.js similarity index 100% rename from apps/cli/template/base/apps/native/tailwind.config.js rename to apps/cli/templates/frontend/native/tailwind.config.js diff --git a/apps/cli/template/base/apps/native/tsconfig.json b/apps/cli/templates/frontend/native/tsconfig.json similarity index 100% rename from apps/cli/template/base/apps/native/tsconfig.json rename to apps/cli/templates/frontend/native/tsconfig.json diff --git a/apps/cli/template/base/apps/native/utils/trpc.ts b/apps/cli/templates/frontend/native/utils/trpc.ts similarity index 100% rename from apps/cli/template/base/apps/native/utils/trpc.ts rename to apps/cli/templates/frontend/native/utils/trpc.ts diff --git a/apps/cli/template/base/apps/web-next/next-env.d.ts b/apps/cli/templates/frontend/next/next-env.d.ts similarity index 100% rename from apps/cli/template/base/apps/web-next/next-env.d.ts rename to apps/cli/templates/frontend/next/next-env.d.ts diff --git a/apps/cli/template/base/apps/web-next/next.config.ts b/apps/cli/templates/frontend/next/next.config.ts similarity index 100% rename from apps/cli/template/base/apps/web-next/next.config.ts rename to apps/cli/templates/frontend/next/next.config.ts diff --git a/apps/cli/template/base/apps/web-next/package.json b/apps/cli/templates/frontend/next/package.json similarity index 93% rename from apps/cli/template/base/apps/web-next/package.json rename to apps/cli/templates/frontend/next/package.json index 120d06c..7329cee 100644 --- a/apps/cli/template/base/apps/web-next/package.json +++ b/apps/cli/templates/frontend/next/package.json @@ -15,8 +15,6 @@ "@radix-ui/react-slot": "^1.2.0", "@tanstack/react-form": "^1.3.2", "@tanstack/react-query": "^5.72.2", - "@trpc/client": "^11.1.0", - "@trpc/tanstack-react-query": "^11.1.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.487.0", diff --git a/apps/cli/template/base/apps/web-next/postcss.config.mjs b/apps/cli/templates/frontend/next/postcss.config.mjs similarity index 100% rename from apps/cli/template/base/apps/web-next/postcss.config.mjs rename to apps/cli/templates/frontend/next/postcss.config.mjs diff --git a/apps/cli/template/base/apps/web-next/src/app/favicon.ico b/apps/cli/templates/frontend/next/src/app/favicon.ico similarity index 100% rename from apps/cli/template/base/apps/web-next/src/app/favicon.ico rename to apps/cli/templates/frontend/next/src/app/favicon.ico diff --git a/apps/cli/template/base/apps/web-next/src/app/layout.tsx b/apps/cli/templates/frontend/next/src/app/layout.tsx similarity index 100% rename from apps/cli/template/base/apps/web-next/src/app/layout.tsx rename to apps/cli/templates/frontend/next/src/app/layout.tsx diff --git a/apps/cli/template/base/apps/web-next/src/app/page.tsx b/apps/cli/templates/frontend/next/src/app/page.tsx.hbs similarity index 93% rename from apps/cli/template/base/apps/web-next/src/app/page.tsx rename to apps/cli/templates/frontend/next/src/app/page.tsx.hbs index afcaa7d..c4d74c6 100644 --- a/apps/cli/template/base/apps/web-next/src/app/page.tsx +++ b/apps/cli/templates/frontend/next/src/app/page.tsx.hbs @@ -1,5 +1,10 @@ "use client" +{{#if (eq api "orpc")}} +import { orpc } from "@/utils/orpc"; +{{/if}} +{{#if (eq api "trpc")}} import { trpc } from "@/utils/trpc"; +{{/if}} import { useQuery } from "@tanstack/react-query"; const TITLE_TEXT = ` @@ -19,7 +24,12 @@ const TITLE_TEXT = ` `; export default function Home() { + {{#if (eq api "orpc")}} + const healthCheck = useQuery(orpc.healthCheck.queryOptions()); + {{/if}} + {{#if (eq api "trpc")}} const healthCheck = useQuery(trpc.healthCheck.queryOptions()); + {{/if}} return (
diff --git a/apps/cli/template/base/apps/web-next/src/components/mode-toggle.tsx b/apps/cli/templates/frontend/next/src/components/mode-toggle.tsx similarity index 100% rename from apps/cli/template/base/apps/web-next/src/components/mode-toggle.tsx rename to apps/cli/templates/frontend/next/src/components/mode-toggle.tsx diff --git a/apps/cli/template/base/apps/web-next/src/components/providers.tsx b/apps/cli/templates/frontend/next/src/components/providers.tsx.hbs similarity index 64% rename from apps/cli/template/base/apps/web-next/src/components/providers.tsx rename to apps/cli/templates/frontend/next/src/components/providers.tsx.hbs index 71896f6..3167d15 100644 --- a/apps/cli/template/base/apps/web-next/src/components/providers.tsx +++ b/apps/cli/templates/frontend/next/src/components/providers.tsx.hbs @@ -1,6 +1,11 @@ "use client" import { QueryClientProvider } from "@tanstack/react-query"; +{{#if (eq api "orpc")}} +import { orpc, ORPCContext, queryClient } from "@/utils/orpc"; +{{/if}} +{{#if (eq api "trpc")}} import { queryClient } from "@/utils/trpc"; +{{/if}} import { ThemeProvider } from "./theme-provider"; import { Toaster } from "./ui/sonner"; @@ -17,7 +22,14 @@ export default function Providers({ disableTransitionOnChange > + {{#if (eq api "orpc")}} + + {children} + + {{/if}} + {{#if (eq api "trpc")}} {children} + {{/if}} diff --git a/apps/cli/template/base/apps/web-next/src/components/theme-provider.tsx b/apps/cli/templates/frontend/next/src/components/theme-provider.tsx similarity index 100% rename from apps/cli/template/base/apps/web-next/src/components/theme-provider.tsx rename to apps/cli/templates/frontend/next/src/components/theme-provider.tsx diff --git a/apps/cli/template/base/apps/web-next/tsconfig.json b/apps/cli/templates/frontend/next/tsconfig.json similarity index 100% rename from apps/cli/template/base/apps/web-next/tsconfig.json rename to apps/cli/templates/frontend/next/tsconfig.json diff --git a/apps/cli/template/base/apps/web-react-router/package.json b/apps/cli/templates/frontend/react-router/package.json similarity index 92% rename from apps/cli/template/base/apps/web-react-router/package.json rename to apps/cli/templates/frontend/react-router/package.json index c45c276..5b1005d 100644 --- a/apps/cli/template/base/apps/web-react-router/package.json +++ b/apps/cli/templates/frontend/react-router/package.json @@ -18,9 +18,6 @@ "@react-router/serve": "^7.4.1", "@tanstack/react-form": "^1.2.3", "@tanstack/react-query": "^5.71.3", - "@trpc/client": "^11.0.1", - "@trpc/server": "^11.0.1", - "@trpc/tanstack-react-query": "^11.0.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "isbot": "^5.1.17", diff --git a/apps/cli/template/base/apps/web-react-router/public/favicon.ico b/apps/cli/templates/frontend/react-router/public/favicon.ico similarity index 100% rename from apps/cli/template/base/apps/web-react-router/public/favicon.ico rename to apps/cli/templates/frontend/react-router/public/favicon.ico diff --git a/apps/cli/template/base/apps/web-react-router/react-router.config.ts b/apps/cli/templates/frontend/react-router/react-router.config.ts similarity index 100% rename from apps/cli/template/base/apps/web-react-router/react-router.config.ts rename to apps/cli/templates/frontend/react-router/react-router.config.ts diff --git a/apps/cli/template/base/apps/web-tanstack-router/src/components/mode-toggle.tsx b/apps/cli/templates/frontend/react-router/src/components/mode-toggle.tsx similarity index 100% rename from apps/cli/template/base/apps/web-tanstack-router/src/components/mode-toggle.tsx rename to apps/cli/templates/frontend/react-router/src/components/mode-toggle.tsx diff --git a/apps/cli/template/base/apps/web-tanstack-router/src/components/theme-provider.tsx b/apps/cli/templates/frontend/react-router/src/components/theme-provider.tsx similarity index 100% rename from apps/cli/template/base/apps/web-tanstack-router/src/components/theme-provider.tsx rename to apps/cli/templates/frontend/react-router/src/components/theme-provider.tsx diff --git a/apps/cli/template/base/apps/web-react-router/src/root.tsx b/apps/cli/templates/frontend/react-router/src/root.tsx.hbs similarity index 77% rename from apps/cli/template/base/apps/web-react-router/src/root.tsx rename to apps/cli/templates/frontend/react-router/src/root.tsx.hbs index 39a43b2..f31f535 100644 --- a/apps/cli/template/base/apps/web-react-router/src/root.tsx +++ b/apps/cli/templates/frontend/react-router/src/root.tsx.hbs @@ -12,9 +12,15 @@ import type { Route } from "./+types/root"; import "./index.css"; import Header from "./components/header"; import { ThemeProvider } from "./components/theme-provider"; -import { queryClient } from "./utils/trpc"; import { Toaster } from "./components/ui/sonner"; +{{#if (eq api "orpc")}} +import { orpc, ORPCContext, queryClient } from "./utils/orpc"; +{{/if}} +{{#if (eq api "trpc")}} +import { queryClient } from "./utils/trpc"; +{{/if}} + export const links: Route.LinksFunction = () => [ { rel: "preconnect", href: "https://fonts.googleapis.com" }, { @@ -46,6 +52,26 @@ export function Layout({ children }: { children: React.ReactNode }) { ); } +{{#if (eq api "orpc")}} +export default function App() { + return ( + + + +
+
+ +
+ +
+
+ +
+ ); +} +{{/if}} + +{{#if (eq api "trpc")}} export default function App() { return ( @@ -60,6 +86,7 @@ export default function App() { ); } +{{/if}} export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) { let message = "Oops!"; diff --git a/apps/cli/template/base/apps/web-react-router/src/routes.ts b/apps/cli/templates/frontend/react-router/src/routes.ts similarity index 100% rename from apps/cli/template/base/apps/web-react-router/src/routes.ts rename to apps/cli/templates/frontend/react-router/src/routes.ts diff --git a/apps/cli/template/base/apps/web-react-router/src/routes/_index.tsx b/apps/cli/templates/frontend/react-router/src/routes/_index.tsx.hbs similarity index 94% rename from apps/cli/template/base/apps/web-react-router/src/routes/_index.tsx rename to apps/cli/templates/frontend/react-router/src/routes/_index.tsx.hbs index b779085..f250a9d 100644 --- a/apps/cli/template/base/apps/web-react-router/src/routes/_index.tsx +++ b/apps/cli/templates/frontend/react-router/src/routes/_index.tsx.hbs @@ -1,5 +1,10 @@ import type { Route } from "./+types/_index"; +{{#if (eq api "orpc")}} +import { orpc } from "@/utils/orpc"; +{{/if}} +{{#if (eq api "trpc")}} import { trpc } from "@/utils/trpc"; +{{/if}} import { useQuery } from "@tanstack/react-query"; const TITLE_TEXT = ` @@ -23,7 +28,13 @@ export function meta({}: Route.MetaArgs) { } export default function Home() { + + {{#if (eq api "orpc")}} + const healthCheck = useQuery(orpc.healthCheck.queryOptions()); + {{/if}} + {{#if (eq api "trpc")}} const healthCheck = useQuery(trpc.healthCheck.queryOptions()); + {{/if}} return (
diff --git a/apps/cli/template/base/apps/web-react-router/tsconfig.json b/apps/cli/templates/frontend/react-router/tsconfig.json similarity index 100% rename from apps/cli/template/base/apps/web-react-router/tsconfig.json rename to apps/cli/templates/frontend/react-router/tsconfig.json diff --git a/apps/cli/templates/frontend/react-router/vite.config.ts.hbs b/apps/cli/templates/frontend/react-router/vite.config.ts.hbs new file mode 100644 index 0000000..4347486 --- /dev/null +++ b/apps/cli/templates/frontend/react-router/vite.config.ts.hbs @@ -0,0 +1,37 @@ +{{! Import VitePWA only if 'pwa' addon is selected }} +{{#if (includes addons "pwa")}} +import { VitePWA } from "vite-plugin-pwa"; +{{/if}} +import { reactRouter } from "@react-router/dev/vite"; +import tailwindcss from "@tailwindcss/vite"; +import { defineConfig } from "vite"; +import tsconfigPaths from "vite-tsconfig-paths"; + +export default defineConfig({ + plugins: [ + tailwindcss(), + reactRouter(), + tsconfigPaths(), + {{! Add VitePWA plugin config only if 'pwa' addon is selected }} + {{#if (includes addons "pwa")}} + VitePWA({ + registerType: "autoUpdate", + manifest: { + // Use context variables for better naming + name: "{{projectName}}", + short_name: "{{projectName}}", + description: "{{projectName}} - PWA Application", + theme_color: "#0c0c0c", + // Add more manifest options as needed + }, + pwaAssets: { + disabled: false, // Set to false to enable asset generation + config: true, // Use pwa-assets.config.ts + }, + devOptions: { + enabled: true, // Enable PWA features in dev mode + }, + }), + {{/if}} + ], +}); diff --git a/apps/cli/template/base/apps/web-tanstack-router/index.html b/apps/cli/templates/frontend/tanstack-router/index.html similarity index 100% rename from apps/cli/template/base/apps/web-tanstack-router/index.html rename to apps/cli/templates/frontend/tanstack-router/index.html diff --git a/apps/cli/template/base/apps/web-tanstack-router/package.json b/apps/cli/templates/frontend/tanstack-router/package.json similarity index 89% rename from apps/cli/template/base/apps/web-tanstack-router/package.json rename to apps/cli/templates/frontend/tanstack-router/package.json index 0010c0d..2d4fb3a 100644 --- a/apps/cli/template/base/apps/web-tanstack-router/package.json +++ b/apps/cli/templates/frontend/tanstack-router/package.json @@ -32,10 +32,6 @@ "@tailwindcss/vite": "^4.0.15", "@tanstack/react-query": "^5.69.0", "@tanstack/react-router": "^1.114.25", - "@trpc/client": "^11.0.0", - "@trpc/react-query": "^11.0.0", - "@trpc/tanstack-react-query": "^11.0.0", - "@trpc/server": "^11.0.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.473.0", diff --git a/apps/cli/template/base/apps/web-react-router/src/components/mode-toggle.tsx b/apps/cli/templates/frontend/tanstack-router/src/components/mode-toggle.tsx similarity index 100% rename from apps/cli/template/base/apps/web-react-router/src/components/mode-toggle.tsx rename to apps/cli/templates/frontend/tanstack-router/src/components/mode-toggle.tsx diff --git a/apps/cli/template/base/apps/web-react-router/src/components/theme-provider.tsx b/apps/cli/templates/frontend/tanstack-router/src/components/theme-provider.tsx similarity index 100% rename from apps/cli/template/base/apps/web-react-router/src/components/theme-provider.tsx rename to apps/cli/templates/frontend/tanstack-router/src/components/theme-provider.tsx diff --git a/apps/cli/template/base/apps/web-tanstack-router/src/main.tsx b/apps/cli/templates/frontend/tanstack-router/src/main.tsx.hbs similarity index 67% rename from apps/cli/template/base/apps/web-tanstack-router/src/main.tsx rename to apps/cli/templates/frontend/tanstack-router/src/main.tsx.hbs index 75af1de..f766884 100644 --- a/apps/cli/template/base/apps/web-tanstack-router/src/main.tsx +++ b/apps/cli/templates/frontend/tanstack-router/src/main.tsx.hbs @@ -3,8 +3,27 @@ import { RouterProvider, createRouter } from "@tanstack/react-router"; import ReactDOM from "react-dom/client"; import Loader from "./components/loader"; import { routeTree } from "./routeTree.gen"; +{{#if (eq api "orpc")}} +import { orpc, queryClient } from "./utils/orpc"; +{{/if}} +{{#if (eq api "trpc")}} import { queryClient, trpc } from "./utils/trpc"; +{{/if}} +{{#if (eq api "orpc")}} +const router = createRouter({ + routeTree, + defaultPreload: "intent", + defaultPendingComponent: () => , + context: { orpc, queryClient }, + Wrap: function WrapComponent({ children }) { + return ( + {children} + ); + }, +}); +{{/if}} +{{#if (eq api "trpc")}} const router = createRouter({ routeTree, defaultPreload: "intent", @@ -16,6 +35,7 @@ const router = createRouter({ ); }, }); +{{/if}} // Register things for typesafety declare module "@tanstack/react-router" { diff --git a/apps/cli/template/base/apps/web-tanstack-router/src/routes/__root.tsx b/apps/cli/templates/frontend/tanstack-router/src/routes/__root.tsx.hbs similarity index 51% rename from apps/cli/template/base/apps/web-tanstack-router/src/routes/__root.tsx rename to apps/cli/templates/frontend/tanstack-router/src/routes/__root.tsx.hbs index 2f47b7d..103628a 100644 --- a/apps/cli/template/base/apps/web-tanstack-router/src/routes/__root.tsx +++ b/apps/cli/templates/frontend/tanstack-router/src/routes/__root.tsx.hbs @@ -2,9 +2,21 @@ import Header from "@/components/header"; import Loader from "@/components/loader"; import { ThemeProvider } from "@/components/theme-provider"; import { Toaster } from "@/components/ui/sonner"; +{{#if (eq api "orpc")}} +import { link, orpc, ORPCContext } from "@/utils/orpc"; +import type { QueryClient } from "@tanstack/react-query"; +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; +import { useState } from "react"; +import type { RouterClient } from "@orpc/server"; +import { createORPCReactQueryUtils } from "@orpc/react-query"; +import type { appRouter } from "../../../server/src/routers"; +import { createORPCClient } from "@orpc/client"; +{{/if}} +{{#if (eq api "trpc")}} import type { trpc } from "@/utils/trpc"; import type { QueryClient } from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; +{{/if}} import { HeadContent, Outlet, @@ -14,10 +26,18 @@ import { import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; import "../index.css"; +{{#if (eq api "orpc")}} +export interface RouterAppContext { + orpc: typeof orpc; + queryClient: QueryClient; +} +{{/if}} +{{#if (eq api "trpc")}} export interface RouterAppContext { trpc: typeof trpc; queryClient: QueryClient; } +{{/if}} export const Route = createRootRouteWithContext()({ component: RootComponent, @@ -40,6 +60,33 @@ export const Route = createRootRouteWithContext()({ }), }); +{{#if (eq api "orpc")}} +function RootComponent() { + const [client] = useState>(() => createORPCClient(link)) + const [orpc] = useState(() => createORPCReactQueryUtils(client)) + + const isFetching = useRouterState({ + select: (s) => s.isLoading, + }); + return ( + <> + + + +
+
+ {isFetching ? : } +
+ +
+
+ + + + ); +} +{{/if}} +{{#if (eq api "trpc")}} function RootComponent() { const isFetching = useRouterState({ select: (s) => s.isLoading, @@ -59,3 +106,4 @@ function RootComponent() { ); } +{{/if}} \ No newline at end of file diff --git a/apps/cli/template/base/apps/web-tanstack-router/src/routes/index.tsx b/apps/cli/templates/frontend/tanstack-router/src/routes/index.tsx.hbs similarity index 93% rename from apps/cli/template/base/apps/web-tanstack-router/src/routes/index.tsx rename to apps/cli/templates/frontend/tanstack-router/src/routes/index.tsx.hbs index c5a33ee..c2f6a82 100644 --- a/apps/cli/template/base/apps/web-tanstack-router/src/routes/index.tsx +++ b/apps/cli/templates/frontend/tanstack-router/src/routes/index.tsx.hbs @@ -1,4 +1,9 @@ +{{#if (eq api "orpc")}} +import { orpc } from "@/utils/orpc"; +{{/if}} +{{#if (eq api "trpc")}} import { trpc } from "@/utils/trpc"; +{{/if}} import { useQuery } from "@tanstack/react-query"; import { createFileRoute } from "@tanstack/react-router"; @@ -23,7 +28,12 @@ const TITLE_TEXT = ` `; function HomeComponent() { + {{#if (eq api "orpc")}} + const healthCheck = useQuery(orpc.healthCheck.queryOptions()); + {{/if}} + {{#if (eq api "trpc")}} const healthCheck = useQuery(trpc.healthCheck.queryOptions()); + {{/if}} return (
diff --git a/apps/cli/template/base/apps/web-tanstack-router/tsconfig.json b/apps/cli/templates/frontend/tanstack-router/tsconfig.json similarity index 100% rename from apps/cli/template/base/apps/web-tanstack-router/tsconfig.json rename to apps/cli/templates/frontend/tanstack-router/tsconfig.json diff --git a/apps/cli/templates/frontend/tanstack-router/vite.config.ts.hbs b/apps/cli/templates/frontend/tanstack-router/vite.config.ts.hbs new file mode 100644 index 0000000..bc99664 --- /dev/null +++ b/apps/cli/templates/frontend/tanstack-router/vite.config.ts.hbs @@ -0,0 +1,43 @@ +{{! Import VitePWA only if 'pwa' addon is selected }} +{{#if (includes addons "pwa")}} +import { VitePWA } from "vite-plugin-pwa"; +{{/if}} +import tailwindcss from "@tailwindcss/vite"; +import { TanStackRouterVite } from "@tanstack/router-plugin/vite"; +import react from "@vitejs/plugin-react"; +import path from "node:path"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [ + tailwindcss(), + TanStackRouterVite({}), + react(), + {{! Add VitePWA plugin config only if 'pwa' addon is selected }} + {{#if (includes addons "pwa")}} + VitePWA({ + registerType: "autoUpdate", + manifest: { + // Use context variables for better naming + name: "{{projectName}}", + short_name: "{{projectName}}", + description: "{{projectName}} - PWA Application", + theme_color: "#0c0c0c", + // Add more manifest options as needed + }, + pwaAssets: { + disabled: false, // Set to false to enable asset generation + config: true, // Use pwa-assets.config.ts + }, + devOptions: { + enabled: true, // Enable PWA features in dev mode + }, + }), + {{/if}} + ], + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, +}); diff --git a/apps/cli/template/base/apps/web-tanstack-start/app.config.ts b/apps/cli/templates/frontend/tanstack-start/app.config.ts similarity index 100% rename from apps/cli/template/base/apps/web-tanstack-start/app.config.ts rename to apps/cli/templates/frontend/tanstack-start/app.config.ts diff --git a/apps/cli/template/base/apps/web-tanstack-start/package.json b/apps/cli/templates/frontend/tanstack-start/package.json similarity index 93% rename from apps/cli/template/base/apps/web-tanstack-start/package.json rename to apps/cli/templates/frontend/tanstack-start/package.json index 6446b2a..405e1c0 100644 --- a/apps/cli/template/base/apps/web-tanstack-start/package.json +++ b/apps/cli/templates/frontend/tanstack-start/package.json @@ -20,9 +20,6 @@ "@tanstack/react-router-with-query": "^1.114.3", "@tanstack/react-start": "^1.114.3", "@tanstack/router-plugin": "^1.114.3", - "@trpc/client": "^11.0.2", - "@trpc/server": "^11.0.2", - "@trpc/tanstack-react-query": "^11.0.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.473.0", diff --git a/apps/cli/template/base/apps/web-tanstack-start/public/robots.txt b/apps/cli/templates/frontend/tanstack-start/public/robots.txt similarity index 100% rename from apps/cli/template/base/apps/web-tanstack-start/public/robots.txt rename to apps/cli/templates/frontend/tanstack-start/public/robots.txt diff --git a/apps/cli/template/base/apps/web-tanstack-start/src/api.ts b/apps/cli/templates/frontend/tanstack-start/src/api.ts similarity index 100% rename from apps/cli/template/base/apps/web-tanstack-start/src/api.ts rename to apps/cli/templates/frontend/tanstack-start/src/api.ts diff --git a/apps/cli/template/base/apps/web-tanstack-start/src/client.tsx b/apps/cli/templates/frontend/tanstack-start/src/client.tsx similarity index 100% rename from apps/cli/template/base/apps/web-tanstack-start/src/client.tsx rename to apps/cli/templates/frontend/tanstack-start/src/client.tsx diff --git a/apps/cli/template/with-auth/apps/web-tanstack-start/src/router.tsx b/apps/cli/templates/frontend/tanstack-start/src/router.tsx.hbs similarity index 76% rename from apps/cli/template/with-auth/apps/web-tanstack-start/src/router.tsx rename to apps/cli/templates/frontend/tanstack-start/src/router.tsx.hbs index 50d7d71..df4c0e0 100644 --- a/apps/cli/template/with-auth/apps/web-tanstack-start/src/router.tsx +++ b/apps/cli/templates/frontend/tanstack-start/src/router.tsx.hbs @@ -4,15 +4,21 @@ import { QueryClientProvider, } from "@tanstack/react-query"; import { createRouter as createTanstackRouter } from "@tanstack/react-router"; +import Loader from "./components/loader"; +import "./index.css"; +import { routeTree } from "./routeTree.gen"; +{{#if (eq api "trpc")}} import { createTRPCClient, httpBatchLink } from "@trpc/client"; import { createTRPCOptionsProxy } from "@trpc/tanstack-react-query"; import { toast } from "sonner"; import type { AppRouter } from "../../server/src/routers"; -import Loader from "./components/loader"; -import "./index.css"; -import { routeTree } from "./routeTree.gen"; import { TRPCProvider } from "./utils/trpc"; +{{/if}} +{{#if (eq api "orpc")}} +import { orpc, ORPCContext, queryClient } from "./utils/orpc"; +{{/if}} +{{#if (eq api "trpc")}} export const queryClient = new QueryClient({ queryCache: new QueryCache({ onError: (error) => { @@ -32,13 +38,19 @@ export const queryClient = new QueryClient({ const trpcClient = createTRPCClient({ links: [ httpBatchLink({ + {{#if (includes frontend 'next')}} + url: `${process.env.NEXT_PUBLIC_SERVER_URL}/trpc`, + {{else}} url: `${import.meta.env.VITE_SERVER_URL}/trpc`, + {{/if}} + {{#if auth}} fetch(url, options) { return fetch(url, { ...options, credentials: "include", }); }, + {{/if}} }), ], }); @@ -47,20 +59,34 @@ const trpc = createTRPCOptionsProxy({ client: trpcClient, queryClient: queryClient, }); +{{/if}} + export const createRouter = () => { const router = createTanstackRouter({ routeTree, scrollRestoration: true, defaultPreloadStaleTime: 0, + {{#if (eq api "trpc")}} context: { trpc, queryClient }, +{{/if}} +{{#if (eq api "orpc")}} + context: { orpc, queryClient }, +{{/if}} defaultPendingComponent: () => , defaultNotFoundComponent: () =>
Not Found
, Wrap: ({ children }) => ( + {{#if (eq api "trpc")}} {children} + {{/if}} + {{#if (eq api "orpc")}} + + {children} + + {{/if}} ), }); diff --git a/apps/cli/template/base/apps/web-tanstack-start/src/routes/__root.tsx b/apps/cli/templates/frontend/tanstack-start/src/routes/__root.tsx.hbs similarity index 87% rename from apps/cli/template/base/apps/web-tanstack-start/src/routes/__root.tsx rename to apps/cli/templates/frontend/tanstack-start/src/routes/__root.tsx.hbs index d44549d..ed6d256 100644 --- a/apps/cli/template/base/apps/web-tanstack-start/src/routes/__root.tsx +++ b/apps/cli/templates/frontend/tanstack-start/src/routes/__root.tsx.hbs @@ -11,14 +11,27 @@ import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; import Header from "../components/header"; import appCss from "../index.css?url"; import type { QueryClient } from "@tanstack/react-query"; +{{#if (eq api "trpc")}} import type { TRPCOptionsProxy } from "@trpc/tanstack-react-query"; import type { AppRouter } from "../../../server/src/routers"; +{{/if}} +{{#if (eq api "orpc")}} +import type { orpc } from "@/utils/orpc"; +{{/if}} import Loader from "@/components/loader"; +{{#if (eq api "trpc")}} export interface RouterAppContext { trpc: TRPCOptionsProxy; queryClient: QueryClient; } +{{/if}} +{{#if (eq api "orpc")}} +export interface RouterAppContext { + orpc: typeof orpc; + queryClient: QueryClient; +} +{{/if}} export const Route = createRootRouteWithContext()({ head: () => ({ diff --git a/apps/cli/template/base/apps/web-tanstack-start/src/routes/index.tsx b/apps/cli/templates/frontend/tanstack-start/src/routes/index.tsx.hbs similarity index 93% rename from apps/cli/template/base/apps/web-tanstack-start/src/routes/index.tsx rename to apps/cli/templates/frontend/tanstack-start/src/routes/index.tsx.hbs index 5bf6e63..0e2a1cf 100644 --- a/apps/cli/template/base/apps/web-tanstack-start/src/routes/index.tsx +++ b/apps/cli/templates/frontend/tanstack-start/src/routes/index.tsx.hbs @@ -1,4 +1,9 @@ +{{#if (eq api "trpc")}} import { useTRPC } from "@/utils/trpc"; +{{/if}} +{{#if (eq api "orpc")}} +import { useORPC } from "@/utils/orpc"; +{{/if}} import { useQuery } from "@tanstack/react-query"; import { createFileRoute } from "@tanstack/react-router"; @@ -23,8 +28,14 @@ const TITLE_TEXT = ` `; function HomeComponent() { + {{#if (eq api "trpc")}} const trpc = useTRPC(); const healthCheck = useQuery(trpc.healthCheck.queryOptions()); + {{/if}} + {{#if (eq api "orpc")}} + const orpc = useORPC(); + const healthCheck = useQuery(orpc.healthCheck.queryOptions()); + {{/if}} return (
diff --git a/apps/cli/template/base/apps/web-tanstack-start/src/ssr.tsx b/apps/cli/templates/frontend/tanstack-start/src/ssr.tsx similarity index 100% rename from apps/cli/template/base/apps/web-tanstack-start/src/ssr.tsx rename to apps/cli/templates/frontend/tanstack-start/src/ssr.tsx diff --git a/apps/cli/template/base/apps/web-tanstack-start/tsconfig.json b/apps/cli/templates/frontend/tanstack-start/tsconfig.json similarity index 100% rename from apps/cli/template/base/apps/web-tanstack-start/tsconfig.json rename to apps/cli/templates/frontend/tanstack-start/tsconfig.json diff --git a/apps/cli/template/base/apps/web-base/_gitignore b/apps/cli/templates/frontend/web-base/_gitignore similarity index 100% rename from apps/cli/template/base/apps/web-base/_gitignore rename to apps/cli/templates/frontend/web-base/_gitignore diff --git a/apps/cli/template/base/apps/web-base/components.json b/apps/cli/templates/frontend/web-base/components.json similarity index 100% rename from apps/cli/template/base/apps/web-base/components.json rename to apps/cli/templates/frontend/web-base/components.json diff --git a/apps/cli/templates/frontend/web-base/src/components/header.tsx.hbs b/apps/cli/templates/frontend/web-base/src/components/header.tsx.hbs new file mode 100644 index 0000000..7da3ad5 --- /dev/null +++ b/apps/cli/templates/frontend/web-base/src/components/header.tsx.hbs @@ -0,0 +1,80 @@ +{{#if (includes frontend "next")}} +"use client"; +import Link from "next/link"; +{{else if (includes frontend "react-router")}} +import { NavLink } from "react-router"; +{{else if (or (includes frontend "tanstack-router") (includes frontend "tanstack-start"))}} +import { Link } from "@tanstack/react-router"; +{{/if}} + +{{#unless (includes frontend "tanstack-start")}} +import { ModeToggle } from "./mode-toggle"; +{{/unless}} +{{#if auth}} +import UserMenu from "./user-menu"; +{{/if}} + +export default function Header() { + const links = [ + { to: "/", label: "Home" }, + {{#if auth}} + { to: "/dashboard", label: "Dashboard" }, + {{/if}} + {{#if (includes examples "todo")}} + { to: "/todos", label: "Todos" }, + {{/if}} + {{#if (includes examples "ai")}} + { to: "/ai", label: "AI Chat" }, + {{/if}} + ]; + + return ( +
+
+ +
+ {{#unless (includes frontend "tanstack-start")}} + + {{/unless}} + {{#if auth}} + + {{/if}} +
+
+
+
+ ); +} \ No newline at end of file diff --git a/apps/cli/template/base/apps/web-base/src/components/loader.tsx b/apps/cli/templates/frontend/web-base/src/components/loader.tsx similarity index 100% rename from apps/cli/template/base/apps/web-base/src/components/loader.tsx rename to apps/cli/templates/frontend/web-base/src/components/loader.tsx diff --git a/apps/cli/template/base/apps/web-base/src/components/ui/button.tsx b/apps/cli/templates/frontend/web-base/src/components/ui/button.tsx similarity index 100% rename from apps/cli/template/base/apps/web-base/src/components/ui/button.tsx rename to apps/cli/templates/frontend/web-base/src/components/ui/button.tsx diff --git a/apps/cli/template/base/apps/web-base/src/components/ui/card.tsx b/apps/cli/templates/frontend/web-base/src/components/ui/card.tsx similarity index 100% rename from apps/cli/template/base/apps/web-base/src/components/ui/card.tsx rename to apps/cli/templates/frontend/web-base/src/components/ui/card.tsx diff --git a/apps/cli/template/base/apps/web-base/src/components/ui/checkbox.tsx b/apps/cli/templates/frontend/web-base/src/components/ui/checkbox.tsx similarity index 100% rename from apps/cli/template/base/apps/web-base/src/components/ui/checkbox.tsx rename to apps/cli/templates/frontend/web-base/src/components/ui/checkbox.tsx diff --git a/apps/cli/template/base/apps/web-base/src/components/ui/dropdown-menu.tsx b/apps/cli/templates/frontend/web-base/src/components/ui/dropdown-menu.tsx similarity index 100% rename from apps/cli/template/base/apps/web-base/src/components/ui/dropdown-menu.tsx rename to apps/cli/templates/frontend/web-base/src/components/ui/dropdown-menu.tsx diff --git a/apps/cli/template/base/apps/web-base/src/components/ui/input.tsx b/apps/cli/templates/frontend/web-base/src/components/ui/input.tsx similarity index 100% rename from apps/cli/template/base/apps/web-base/src/components/ui/input.tsx rename to apps/cli/templates/frontend/web-base/src/components/ui/input.tsx diff --git a/apps/cli/template/base/apps/web-base/src/components/ui/label.tsx b/apps/cli/templates/frontend/web-base/src/components/ui/label.tsx similarity index 100% rename from apps/cli/template/base/apps/web-base/src/components/ui/label.tsx rename to apps/cli/templates/frontend/web-base/src/components/ui/label.tsx diff --git a/apps/cli/template/base/apps/web-base/src/components/ui/skeleton.tsx b/apps/cli/templates/frontend/web-base/src/components/ui/skeleton.tsx similarity index 100% rename from apps/cli/template/base/apps/web-base/src/components/ui/skeleton.tsx rename to apps/cli/templates/frontend/web-base/src/components/ui/skeleton.tsx diff --git a/apps/cli/template/base/apps/web-base/src/components/ui/sonner.tsx b/apps/cli/templates/frontend/web-base/src/components/ui/sonner.tsx similarity index 100% rename from apps/cli/template/base/apps/web-base/src/components/ui/sonner.tsx rename to apps/cli/templates/frontend/web-base/src/components/ui/sonner.tsx diff --git a/apps/cli/template/base/apps/web-base/src/index.css b/apps/cli/templates/frontend/web-base/src/index.css similarity index 100% rename from apps/cli/template/base/apps/web-base/src/index.css rename to apps/cli/templates/frontend/web-base/src/index.css diff --git a/apps/cli/template/base/apps/web-base/src/lib/utils.ts b/apps/cli/templates/frontend/web-base/src/lib/utils.ts similarity index 100% rename from apps/cli/template/base/apps/web-base/src/lib/utils.ts rename to apps/cli/templates/frontend/web-base/src/lib/utils.ts diff --git a/apps/web/package.json b/apps/web/package.json index a39f1ca..6f52db1 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -18,7 +18,7 @@ "fumadocs-mdx": "11.5.7", "fumadocs-ui": "15.1.2", "lucide-react": "^0.485.0", - "motion": "^12.6.3", + "motion": "^12.7.4", "next": "15.2.3", "react": "^19.1.0", "react-dom": "^19.1.0", diff --git a/apps/web/src/app/(home)/_components/CodeContainer.tsx b/apps/web/src/app/(home)/_components/CodeContainer.tsx index 07390ec..8299283 100644 --- a/apps/web/src/app/(home)/_components/CodeContainer.tsx +++ b/apps/web/src/app/(home)/_components/CodeContainer.tsx @@ -1,6 +1,6 @@ "use client"; -import { motion } from "framer-motion"; import { Check, ClipboardCopy, Terminal } from "lucide-react"; +import { motion } from "motion/react"; import { useEffect, useRef, useState } from "react"; const CodeContainer = () => { diff --git a/apps/web/src/app/(home)/_components/CustomizableSection.tsx b/apps/web/src/app/(home)/_components/CustomizableSection.tsx index 0313398..2d94c14 100644 --- a/apps/web/src/app/(home)/_components/CustomizableSection.tsx +++ b/apps/web/src/app/(home)/_components/CustomizableSection.tsx @@ -1,5 +1,5 @@ -import { motion } from "framer-motion"; import { Code, Sliders, Terminal, TerminalSquare } from "lucide-react"; +import { motion } from "motion/react"; import StackArchitect from "./StackArchitech"; export default function CustomizableSection() { diff --git a/apps/web/src/app/(home)/_components/StackArchitech.tsx b/apps/web/src/app/(home)/_components/StackArchitech.tsx index d1c0e43..7a1a84a 100644 --- a/apps/web/src/app/(home)/_components/StackArchitech.tsx +++ b/apps/web/src/app/(home)/_components/StackArchitech.tsx @@ -6,7 +6,6 @@ import { type StackState, TECH_OPTIONS, } from "@/lib/constant"; -import { motion } from "framer-motion"; import { Check, Circle, @@ -20,8 +19,9 @@ import { Star, Terminal, } from "lucide-react"; +import { motion } from "motion/react"; import Link from "next/link"; -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; const validateProjectName = (name: string): string | undefined => { const INVALID_CHARS = ["<", ">", ":", '"', "|", "?", "*"]; @@ -67,6 +67,27 @@ const StackArchitect = ({ const [showHelp, setShowHelp] = useState(false); const [lastSavedStack, setLastSavedStack] = useState(null); + const hasNativeFrontend = useMemo( + () => stack.frontend.includes("native"), + [stack.frontend], + ); + const hasWebFrontend = useMemo( + () => + stack.frontend.some((f) => + ["tanstack-router", "react-router", "tanstack-start", "next"].includes( + f, + ), + ), + [stack.frontend], + ); + const hasPWACompatibleFrontend = useMemo( + () => + stack.frontend.some((f) => + ["tanstack-router", "react-router"].includes(f), + ), + [stack.frontend], + ); + useEffect(() => { const savedStack = localStorage.getItem("betterTStackPreference"); if (savedStack) { @@ -79,128 +100,157 @@ const StackArchitect = ({ } }, []); + // biome-ignore lint/correctness/useExhaustiveDependencies: useEffect(() => { - if (stack.database === "none") { - if (stack.orm !== "none") { - setStack((prev) => ({ ...prev, orm: "none" })); - } - if (stack.auth === "true") { - setStack((prev) => ({ ...prev, auth: "false" })); - } - if (stack.dbSetup !== "none") { - setStack((prev) => ({ ...prev, dbSetup: "none" })); - } - } + let changed = false; + const nextStack = { ...stack }; + const originalAuth = stack.auth; - if (stack.database === "mongodb" && stack.orm === "drizzle") { - setStack((prev) => ({ ...prev, orm: "prisma" })); - } - - if (stack.dbSetup === "turso") { - if (stack.database !== "sqlite") { - setStack((prev) => ({ ...prev, database: "sqlite" })); + if (nextStack.database === "none") { + if (nextStack.orm !== "none") { + nextStack.orm = "none"; + changed = true; } - if (stack.orm === "prisma") { - setStack((prev) => ({ ...prev, orm: "drizzle" })); + if (nextStack.auth === "true") { + nextStack.auth = "false"; + changed = true; } - } else if (stack.dbSetup === "prisma-postgres") { - if (stack.database !== "postgres") { - setStack((prev) => ({ ...prev, database: "postgres" })); + if (nextStack.dbSetup !== "none") { + nextStack.dbSetup = "none"; + changed = true; } - if (stack.orm !== "prisma") { - setStack((prev) => ({ ...prev, orm: "prisma" })); - } - } else if (stack.dbSetup === "mongodb-atlas") { - if (stack.database !== "mongodb") { - setStack((prev) => ({ ...prev, database: "mongodb" })); - } - if (stack.orm !== "prisma") { - setStack((prev) => ({ ...prev, orm: "prisma" })); - } - } else if (stack.dbSetup === "neon") { - if (stack.database !== "postgres") { - setStack((prev) => ({ ...prev, database: "postgres" })); - } - } - }, [stack.database, stack.orm, stack.dbSetup, stack.auth]); - - useEffect(() => { - const cmd = generateCommand(stack); - setCommand(cmd); - - const notes: Record = {}; - const hasWebFrontend = - stack.frontend.includes("tanstack-router") || - stack.frontend.includes("react-router") || - stack.frontend.includes("tanstack-start") || - stack.frontend.includes("next"); - - notes.frontend = []; - - notes.dbSetup = []; - if (stack.database === "none") { - notes.dbSetup.push("Database setup requires a database."); } else { - if (stack.dbSetup === "turso") { - if (stack.database !== "sqlite") { - notes.dbSetup.push("Turso setup requires SQLite database."); - } - if (stack.orm === "prisma") { - notes.dbSetup.push("Turso is not compatible with Prisma ORM."); - } - } else if (stack.dbSetup === "prisma-postgres") { - if (stack.database !== "postgres") { - notes.dbSetup.push( - "Prisma PostgreSQL setup requires PostgreSQL database.", - ); - } - if (stack.orm !== "prisma") { - notes.dbSetup.push("Prisma PostgreSQL setup requires Prisma ORM."); - } - } else if (stack.dbSetup === "mongodb-atlas") { - if (stack.database !== "mongodb") { - notes.dbSetup.push("MongoDB Atlas setup requires MongoDB database."); - } - } else if (stack.dbSetup === "neon") { - if (stack.database !== "postgres") { - notes.dbSetup.push("Neon setup requires PostgreSQL database."); - } + if ( + nextStack.auth === "false" && + (hasWebFrontend || hasNativeFrontend) && + originalAuth === "false" + ) { } } - notes.addons = []; - if (!hasWebFrontend) { - notes.addons.push("PWA and Tauri are only available with React Web."); + if (nextStack.database === "mongodb" && nextStack.orm === "drizzle") { + nextStack.orm = "prisma"; + changed = true; } - notes.database = []; + if (nextStack.dbSetup === "turso") { + if (nextStack.database !== "sqlite") { + nextStack.database = "sqlite"; + changed = true; + } + if (nextStack.orm === "prisma") { + nextStack.orm = "drizzle"; + changed = true; + } + } else if (nextStack.dbSetup === "prisma-postgres") { + if (nextStack.database !== "postgres") { + nextStack.database = "postgres"; + changed = true; + } + if (nextStack.orm !== "prisma") { + nextStack.orm = "prisma"; + changed = true; + } + } else if (nextStack.dbSetup === "mongodb-atlas") { + if (nextStack.database !== "mongodb") { + nextStack.database = "mongodb"; + changed = true; + } + if (nextStack.orm !== "prisma") { + nextStack.orm = "prisma"; + changed = true; + } + } else if (nextStack.dbSetup === "neon") { + if (nextStack.database !== "postgres") { + nextStack.database = "postgres"; + changed = true; + } + } - notes.orm = []; - if (stack.database === "none") { - notes.orm.push( - "ORM options are only available when a database is selected.", + if (changed) { + setStack((currentStack) => ({ + ...currentStack, + database: nextStack.database, + orm: nextStack.orm, + auth: nextStack.auth, + dbSetup: nextStack.dbSetup, + })); + } + }, [ + stack.database, + stack.orm, + stack.dbSetup, + stack.auth, + hasWebFrontend, + hasNativeFrontend, + ]); + + useEffect(() => { + let addonsChanged = false; + let examplesChanged = false; + let apiChanged = false; + + const currentAddons = stack.addons; + const currentExamples = stack.examples; + const currentApi = stack.api; + const currentBackend = stack.backendFramework; + + let nextAddons = [...currentAddons]; + let nextExamples = [...currentExamples]; + let nextApi = currentApi; + + if (!hasPWACompatibleFrontend) { + const incompatibleAddons = ["pwa", "tauri"]; + const originalLength = nextAddons.length; + nextAddons = nextAddons.filter( + (addon) => !incompatibleAddons.includes(addon), ); - } else if (stack.database === "mongodb" && stack.orm === "drizzle") { - notes.orm.push("MongoDB is only available with Prisma ORM."); + if (nextAddons.length !== originalLength) { + addonsChanged = true; + } } - notes.auth = []; - if (stack.database === "none") { - notes.auth.push("Authentication requires a database."); - } - - notes.examples = []; if (!hasWebFrontend) { - notes.examples.push( - "Todo and AI examples are only available with React Web.", + const incompatibleExamples = ["todo", "ai"]; + const originalLength = nextExamples.length; + nextExamples = nextExamples.filter( + (example) => !incompatibleExamples.includes(example), ); - } - if (stack.backendFramework === "elysia") { - notes.examples.push("AI example is only compatible with Hono backend."); + if (nextExamples.length !== originalLength) { + examplesChanged = true; + } } - setCompatNotes(notes); - }, [stack]); + if (currentBackend === "elysia") { + const originalLength = nextExamples.length; + nextExamples = nextExamples.filter((example) => example !== "ai"); + if (nextExamples.length !== originalLength) { + examplesChanged = true; + } + } + + if (hasNativeFrontend && currentApi !== "trpc") { + nextApi = "trpc"; + apiChanged = true; + } + + if (addonsChanged || examplesChanged || apiChanged) { + setStack((prev) => ({ + ...prev, + addons: addonsChanged ? nextAddons : prev.addons, + examples: examplesChanged ? nextExamples : prev.examples, + api: apiChanged ? nextApi : prev.api, + })); + } + }, [ + stack.addons, + stack.examples, + stack.api, + stack.backendFramework, + hasPWACompatibleFrontend, + hasWebFrontend, + hasNativeFrontend, + ]); const generateCommand = useCallback((stackState: StackState) => { let base: string; @@ -215,7 +265,10 @@ const StackArchitect = ({ const projectName = stackState.projectName || "my-better-t-app"; const flags: string[] = ["--yes"]; - if (stackState.frontend.length === 1 && stackState.frontend[0] === "none") { + if ( + stackState.frontend.length === 0 || + (stackState.frontend.length === 1 && stackState.frontend[0] === "none") + ) { flags.push("--frontend none"); } else if ( !( @@ -270,14 +323,129 @@ const StackArchitect = ({ flags.push(`--examples ${stackState.examples.join(" ")}`); } + if (stackState.api && stackState.api !== "trpc") { + flags.push(`--api ${stackState.api}`); + } + return `${base} ${projectName} ${flags.join(" ")}`; }, []); + useEffect(() => { + const cmd = generateCommand(stack); + setCommand(cmd); + + const notes: Record = {}; + + notes.frontend = []; + if (stack.frontend.includes("native") && stack.frontend.length > 1) { + notes.frontend.push( + "When using React Native alongside a web frontend, only the tRPC API option is available.", + ); + } + + notes.dbSetup = []; + if (stack.database === "none") { + notes.dbSetup.push("Database setup requires a database to be selected."); + } else { + if (stack.dbSetup === "turso") { + if (stack.database !== "sqlite") { + notes.dbSetup.push("Turso setup requires the SQLite database."); + } + if (stack.orm === "prisma") { + notes.dbSetup.push("Turso is not compatible with the Prisma ORM."); + } + } else if (stack.dbSetup === "prisma-postgres") { + if (stack.database !== "postgres") { + notes.dbSetup.push( + "Prisma PostgreSQL setup requires the PostgreSQL database.", + ); + } + if (stack.orm !== "prisma") { + notes.dbSetup.push( + "Prisma PostgreSQL setup requires the Prisma ORM.", + ); + } + } else if (stack.dbSetup === "mongodb-atlas") { + if (stack.database !== "mongodb") { + notes.dbSetup.push( + "MongoDB Atlas setup requires the MongoDB database.", + ); + } + } else if (stack.dbSetup === "neon") { + if (stack.database !== "postgres") { + notes.dbSetup.push("Neon setup requires the PostgreSQL database."); + } + } + } + + notes.addons = []; + if (!hasPWACompatibleFrontend) { + notes.addons.push( + "PWA and Tauri addons require TanStack Router or React Router.", + ); + } + if (stack.addons.includes("husky") && !stack.addons.includes("biome")) { + notes.addons.push( + "Husky addon automatically enables Biome for lint-staged.", + ); + } + + notes.database = []; + if (stack.database === "mongodb" && stack.orm === "drizzle") { + notes.database.push("MongoDB is only compatible with the Prisma ORM."); + } + + notes.orm = []; + if (stack.database === "none") { + notes.orm.push("ORM options require a database to be selected."); + } else if (stack.database === "mongodb" && stack.orm === "drizzle") { + notes.orm.push("MongoDB is only compatible with the Prisma ORM."); + } else if ( + stack.database === "sqlite" && + stack.orm === "prisma" && + stack.dbSetup === "turso" + ) { + notes.orm.push("Prisma ORM is not compatible with the Turso DB setup."); + } + + notes.auth = []; + if (stack.database === "none") { + notes.auth.push("Authentication requires a database."); + } + + notes.examples = []; + if (!hasWebFrontend) { + notes.examples.push( + "Todo and AI examples require a web frontend (TanStack Router, React Router, TanStack Start, or Next.js).", + ); + } + if (stack.backendFramework === "elysia" && stack.examples.includes("ai")) { + notes.examples.push( + "The AI example is currently only compatible with the Hono backend.", + ); + } + + notes.api = []; + if (hasNativeFrontend && stack.api !== "trpc") { + notes.api.push("React Native frontend requires the tRPC API option."); + } + + setCompatNotes(notes); + }, [ + stack, + hasWebFrontend, + hasPWACompatibleFrontend, + hasNativeFrontend, + generateCommand, + ]); + const handleTechSelect = useCallback( (category: keyof typeof TECH_OPTIONS, techId: string) => { setStack((prev) => { + const currentStack = { ...prev }; + if (category === "frontend") { - const currentSelection = [...prev.frontend]; + let currentSelection = [...currentStack.frontend]; const webTypes = [ "tanstack-router", "react-router", @@ -286,30 +454,28 @@ const StackArchitect = ({ ]; if (techId === "none") { - return { - ...prev, - frontend: ["none"], - examples: [], - addons: prev.addons.filter( - (addon) => addon !== "pwa" && addon !== "tauri", - ), - }; + return { ...currentStack, frontend: ["none"] }; + } + + if ( + currentSelection.includes(techId) && + currentSelection.length === 1 + ) { + return prev; } if (currentSelection.includes(techId)) { - if (currentSelection.length === 1) { - return prev; + currentSelection = currentSelection.filter((id) => id !== techId); + + if (currentSelection.length === 0) { } - return { - ...prev, - frontend: currentSelection.filter((id) => id !== techId), - }; + return { ...currentStack, frontend: currentSelection }; } let newSelection = [...currentSelection]; if (newSelection.includes("none")) { - newSelection = []; + newSelection = newSelection.filter((id) => id !== "none"); } if (webTypes.includes(techId)) { @@ -318,173 +484,104 @@ const StackArchitect = ({ newSelection.push(techId); - return { - ...prev, - frontend: newSelection, - }; + return { ...currentStack, frontend: newSelection }; } if (category === "addons" || category === "examples") { - const currentArray = [...(prev[category] || [])]; + const currentArray = [...(currentStack[category] || [])]; const index = currentArray.indexOf(techId); - const hasWebFrontend = - prev.frontend.includes("tanstack-router") || - prev.frontend.includes("react-router") || - prev.frontend.includes("tanstack-start"); - - const hasPWACompatibleFrontend = - prev.frontend.includes("tanstack-router") || - prev.frontend.includes("react-router"); if (index >= 0) { currentArray.splice(index, 1); } else { - if ( - category === "examples" && - (techId === "todo" || techId === "ai") && - !hasWebFrontend - ) { - return prev; + if (category === "examples") { + if (!hasWebFrontend && (techId === "todo" || techId === "ai")) + return prev; + if (techId === "ai" && currentStack.backendFramework === "elysia") + return prev; } - - if ( - category === "examples" && - techId === "ai" && - prev.backendFramework === "elysia" - ) { - return prev; + if (category === "addons") { + if ( + !hasPWACompatibleFrontend && + (techId === "pwa" || techId === "tauri") + ) + return prev; + if (techId === "husky" && !currentArray.includes("biome")) { + currentArray.push("biome"); + } } - - if ( - category === "addons" && - (techId === "pwa" || techId === "tauri") && - !hasPWACompatibleFrontend - ) { - return prev; - } - - if ( - category === "addons" && - techId === "husky" && - !currentArray.includes("biome") - ) { - currentArray.push("biome"); - } - currentArray.push(techId); } - - return { - ...prev, - [category]: currentArray, - }; + return { ...currentStack, [category]: currentArray }; } if (category === "database") { - let updatedState = { ...prev, database: techId }; + if (currentStack.database === techId) return prev; + + const updatedState = { ...currentStack, database: techId }; if (techId === "none") { - updatedState = { - ...updatedState, - orm: "none", - dbSetup: "none", - auth: "false", - }; } else if (prev.database === "none") { updatedState.orm = techId === "mongodb" ? "prisma" : "drizzle"; updatedState.dbSetup = "none"; - - const hasCompatibleFrontend = - prev.frontend.length > 0 && !prev.frontend.includes("none"); - if (hasCompatibleFrontend) { - updatedState.auth = "true"; - } } else { - if (techId === "mongodb" && updatedState.orm === "drizzle") { - updatedState.orm = "prisma"; - } - - if (updatedState.dbSetup !== "none") { - if ( - (updatedState.dbSetup === "turso" && techId !== "sqlite") || - (updatedState.dbSetup === "prisma-postgres" && - techId !== "postgres") || - (updatedState.dbSetup === "mongodb-atlas" && - techId !== "mongodb") || - (updatedState.dbSetup === "neon" && techId !== "postgres") - ) { - updatedState.dbSetup = "none"; - } - } } - return updatedState; } if (category === "orm") { - if (prev.database === "none") { + if (currentStack.database === "none") return prev; + if (currentStack.database === "mongodb" && techId === "drizzle") + return prev; + if ( + currentStack.database === "sqlite" && + techId === "prisma" && + currentStack.dbSetup === "turso" + ) return prev; - } - const updatedState = { - ...prev, - orm: techId, - }; + if (currentStack.orm === techId) return prev; - if (updatedState.dbSetup !== "none") { - if ( - (updatedState.dbSetup === "turso" && techId === "prisma") || - (updatedState.dbSetup === "prisma-postgres" && - techId !== "prisma") - ) { - updatedState.dbSetup = "none"; - } - } - - return updatedState; + return { ...currentStack, orm: techId }; } if (category === "dbSetup") { - if (prev.database === "none" && techId !== "none") { + if (currentStack.database === "none" && techId !== "none") return prev; - } - const updatedState = { - ...prev, - dbSetup: techId, - }; + if (currentStack.dbSetup === techId) return prev; - if (techId === "turso") { - updatedState.database = "sqlite"; - updatedState.orm = "drizzle"; - } else if (techId === "prisma-postgres") { - updatedState.database = "postgres"; - updatedState.orm = "prisma"; - } else if (techId === "mongodb-atlas") { - updatedState.database = "mongodb"; - updatedState.orm = "prisma"; - } else if (techId === "neon") { - updatedState.database = "postgres"; - } + return { ...currentStack, dbSetup: techId }; + } - return updatedState; + if (category === "auth") { + if (currentStack.database === "none" && techId === "true") + return prev; + if (currentStack.auth === techId) return prev; + return { ...currentStack, auth: techId }; + } + + if (category === "api") { + if (hasNativeFrontend && techId !== "trpc") return prev; + if (currentStack.api === techId) return prev; + return { ...currentStack, api: techId }; } if ( - category === "auth" && - prev.database === "none" && - techId === "true" + category === "runtime" || + category === "backendFramework" || + category === "packageManager" || + category === "git" || + category === "install" ) { - return prev; + if (currentStack[category] === techId) return prev; + return { ...currentStack, [category]: techId }; } - return { - ...prev, - [category]: techId, - }; + return prev; }); }, - [], + [hasWebFrontend, hasPWACompatibleFrontend, hasNativeFrontend], ); const copyToClipboard = useCallback(() => { @@ -496,24 +593,34 @@ const StackArchitect = ({ const resetStack = useCallback(() => { setStack(DEFAULT_STACK); setActiveTab("frontend"); + setProjectNameError(undefined); }, []); const saveCurrentStack = useCallback(() => { localStorage.setItem("betterTStackPreference", JSON.stringify(stack)); setLastSavedStack(stack); - const saveMessage = document.createElement("div"); - saveMessage.textContent = "Stack preferences saved!"; - saveMessage.className = - "fixed bottom-4 right-4 bg-green-500 text-white py-2 px-4 rounded-md shadow-lg z-50"; - document.body.appendChild(saveMessage); - setTimeout(() => { - document.body.removeChild(saveMessage); - }, 3000); + setCopied(false); + const saveButton = document.getElementById("save-stack-button"); + const saveTextSpan = saveButton?.querySelector("span"); + + if (saveButton && saveTextSpan) { + const originalText = saveTextSpan.textContent; + saveTextSpan.textContent = "Saved!"; + + setTimeout(() => { + if (saveTextSpan.textContent === "Saved!") { + saveTextSpan.textContent = originalText; + } + }, 2000); + } }, [stack]); const loadSavedStack = useCallback(() => { if (lastSavedStack) { setStack(lastSavedStack); + setProjectNameError( + validateProjectName(lastSavedStack.projectName || ""), + ); } }, [lastSavedStack]); @@ -523,87 +630,177 @@ const StackArchitect = ({ ); if (preset) { setStack(preset.stack); + setProjectNameError(validateProjectName(preset.stack.projectName || "")); setShowPresets(false); } }, []); + const getDisabledReason = useCallback( + (category: keyof typeof TECH_OPTIONS, techId: string): string | null => { + if (category === "api" && techId !== "trpc" && hasNativeFrontend) { + return "Only tRPC API is supported when React Native is selected."; + } + if (category === "orm") { + if (stack.database === "none") + return "Select a database to enable ORM options."; + if (stack.database === "mongodb" && techId === "drizzle") + return "MongoDB requires the Prisma ORM."; + if ( + stack.database === "sqlite" && + techId === "prisma" && + stack.dbSetup === "turso" + ) + return "Prisma ORM is not compatible with Turso."; + } + if (category === "dbSetup" && techId !== "none") { + if (stack.database === "none") + return "Select a database before choosing a cloud setup."; + + if (techId === "turso") { + if (stack.orm === "prisma") + return "Turso is not compatible with the Prisma ORM."; + } + } + if ( + category === "auth" && + techId === "true" && + stack.database === "none" + ) { + return "Authentication requires a database."; + } + if (category === "addons") { + if ( + (techId === "pwa" || techId === "tauri") && + !hasPWACompatibleFrontend + ) { + return "Requires TanStack Router or React Router frontend."; + } + } + if (category === "examples") { + if ((techId === "todo" || techId === "ai") && !hasWebFrontend) { + return "Requires a web frontend (TanStack Router, React Router, etc.)."; + } + if (techId === "ai" && stack.backendFramework === "elysia") { + return "AI example is not compatible with Elysia backend."; + } + } + + return null; + }, + [ + stack.database, + stack.orm, + stack.dbSetup, + stack.backendFramework, + hasNativeFrontend, + hasPWACompatibleFrontend, + hasWebFrontend, + ], + ); + return ( -
-
-
-
-
-
-
-
-
- Stack Architect Terminal -
-
- - - - - - + + {!fullscreen && ( + + + )} - + +
{showHelp && ( -
+

How to Use Stack Architect

  • Select your preferred technologies from each category using the - tabs below + tabs below.
  • - The command will automatically update based on your selections + Some selections may disable or automatically change other + options based on compatibility.
  • - Click the copy button to copy the command to your clipboard + The command will automatically update based on your selections.
  • - You can reset to defaults or choose from presets for quick setup + Click the copy button ( + ) + to copy the command. +
  • +
  • + Use presets ( + ) for quick + setup or reset ( + ) to + defaults. +
  • +
  • + Save () + your preferences to load ( + ) them + later.
  • -
  • Save your preferences to load them later when you return
)} - {showPresets && ( -
+

Quick Start Presets

@@ -627,9 +824,9 @@ const StackArchitect = ({
)} -
-
-
-
- {Object.keys(TECH_OPTIONS).map((category) => ( + +
+ {[ + "frontend", + "runtime", + "backendFramework", + "api", + "database", + "orm", + "dbSetup", + "auth", + "packageManager", + "addons", + "examples", + "git", + "install", + ].map((category) => ( ))}
diff --git a/apps/web/src/app/(home)/_components/TechShowcase.tsx b/apps/web/src/app/(home)/_components/TechShowcase.tsx index a2456e4..569498b 100644 --- a/apps/web/src/app/(home)/_components/TechShowcase.tsx +++ b/apps/web/src/app/(home)/_components/TechShowcase.tsx @@ -1,4 +1,4 @@ -import { motion } from "framer-motion"; +import { motion } from "motion/react"; import React, { useState } from "react"; interface TechItem { diff --git a/apps/web/src/app/(home)/_components/Testimonials.tsx b/apps/web/src/app/(home)/_components/Testimonials.tsx index 04b0f69..f17ab6e 100644 --- a/apps/web/src/app/(home)/_components/Testimonials.tsx +++ b/apps/web/src/app/(home)/_components/Testimonials.tsx @@ -1,7 +1,7 @@ "use client"; -import { motion } from "framer-motion"; import { ChevronLeft, ChevronRight } from "lucide-react"; +import { motion } from "motion/react"; import { useEffect, useState } from "react"; import { Tweet } from "react-tweet"; diff --git a/apps/web/src/app/(home)/layout.tsx b/apps/web/src/app/(home)/layout.tsx index 1fa8897..a6ff2d8 100644 --- a/apps/web/src/app/(home)/layout.tsx +++ b/apps/web/src/app/(home)/layout.tsx @@ -1,13 +1,9 @@ import type { ReactNode } from "react"; -import Footer from "./_components/Footer"; -import Navbar from "./_components/Navbar"; export default function Layout({ children }: { children: ReactNode }) { return (
- -
{children}
-
+ {children}
); } diff --git a/apps/web/src/app/(home)/new/page.tsx b/apps/web/src/app/(home)/new/page.tsx index 0ca26c6..cf93b7f 100644 --- a/apps/web/src/app/(home)/new/page.tsx +++ b/apps/web/src/app/(home)/new/page.tsx @@ -1,6 +1,7 @@ "use client"; -import { motion } from "framer-motion"; +import { motion } from "motion/react"; +import Link from "next/link"; import { useEffect } from "react"; import StackArchitect from "../_components/StackArchitech"; @@ -13,44 +14,34 @@ export default function FullScreenStackArchitect() { setVh(); window.addEventListener("resize", setVh); - return () => window.removeEventListener("resize", setVh); + document.body.style.overflow = "hidden"; + + return () => { + window.removeEventListener("resize", setVh); + document.body.style.overflow = ""; + }; }, []); return ( -
+
-
-
-

- Design Your Ideal Full Stack -

-

- Configure every aspect of your TypeScript application with the - interactive stack architect. Choose your technologies, add - features, and generate your startup command. -

-
- - - -
-

- Need help getting started? -

-

- Select a preset template for common configurations, or customize - each component of your stack. When you are ready, copy the - generated command and run it in your terminal to create your - project. -

-
-
+
+ +
+

+ Tip: Use presets (⭐), save (💾), reset (🔄), or copy (📋) the + command.{" "} + + Exit Fullscreen + +

+
); } diff --git a/apps/web/src/app/(home)/page.tsx b/apps/web/src/app/(home)/page.tsx index 3dd3aba..c7777a2 100644 --- a/apps/web/src/app/(home)/page.tsx +++ b/apps/web/src/app/(home)/page.tsx @@ -1,86 +1,166 @@ "use client"; import ShinyText from "@/app/(home)/_components/ShinyText"; +import { motion } from "motion/react"; import React from "react"; import BackgroundGradients from "./_components/BackgroundGradients"; import CodeContainer from "./_components/CodeContainer"; import CustomizableSection from "./_components/CustomizableSection"; +import Footer from "./_components/Footer"; +import Navbar from "./_components/Navbar"; import NpmPackage from "./_components/NpmPackage"; import Testimonials from "./_components/Testimonials"; export default function HomePage() { + const containerVariants = { + hidden: { opacity: 0 }, + visible: { + opacity: 1, + transition: { + staggerChildren: 0.15, + }, + }, + }; + + const itemVariants = { + hidden: { opacity: 0, y: 20 }, + visible: { + opacity: 1, + y: 0, + transition: { + duration: 0.5, + ease: "easeOut", + }, + }, + }; + + const sectionVariants = { + hidden: { opacity: 0, y: 30 }, + visible: { + opacity: 1, + y: 0, + transition: { + duration: 0.6, + ease: "easeOut", + }, + }, + }; + return ( -
- -
-
-
-

- - Better-T Stack - -

+ <> + +
+ -
- + +
+
+ + + Better-T Stack + + + + + + + + + A modern CLI tool for scaffolding end-to-end type-safe + TypeScript projects with best practices and customizable + configurations + + + + + + + + +
- -

- A modern CLI tool for scaffolding end-to-end type-safe TypeScript - projects with best practices and customizable configurations -

- -
- -
- -
-
-
+ - + + + -
-
-
-
-
-
-
+ +
+
+
+
+
+
-
-
- - Code Icon - - +
+
+ + Code Icon + + +
+
+ +
+
+
- -
-
-
-
+
+ -
-
-
- - -
+ + + + +