diff --git a/.changeset/khaki-suns-sit.md b/.changeset/khaki-suns-sit.md new file mode 100644 index 0000000..e8e7423 --- /dev/null +++ b/.changeset/khaki-suns-sit.md @@ -0,0 +1,5 @@ +--- +"create-better-t-stack": minor +--- + +Added support for building mobile applications with Expo diff --git a/README.md b/README.md index e05ce9b..824f956 100644 --- a/README.md +++ b/README.md @@ -20,10 +20,11 @@ pnpm create better-t-stack@latest - ⚡️ **Zero-config setup** with interactive CLI wizard - 🔄 **End-to-end type safety** from database to frontend via tRPC - 🧱 **Modern stack** with React, Hono/Elysia, and TanStack libraries +- 📱 **Multi-platform** supporting web, mobile (Expo), and desktop applications - 🗃️ **Database flexibility** with SQLite (Turso) or PostgreSQL options - 🛠️ **ORM choice** between Drizzle or Prisma - 🔒 **Built-in authentication** with Better-Auth -- 📱 **Optional PWA support** for mobile-friendly applications +- 📱 **Optional PWA support** for installable web applications - 🖥️ **Desktop app capabilities** with Tauri integration - 📦 **Monorepo architecture** powered by Turborepo diff --git a/apps/cli/README.md b/apps/cli/README.md index a2fbbb4..2e557f5 100644 --- a/apps/cli/README.md +++ b/apps/cli/README.md @@ -1,6 +1,6 @@ # Create Better-T-Stack CLI -An interactive CLI tool to quickly scaffold full-stack TypeScript applications with a choice of modern backend frameworks (Hono or Elysia) and tRPC. The Better-T-Stack provides a type-safe development experience with the best tools from the TypeScript ecosystem. +A CLI tool for scaffolding type-safe full-stack apps with Hono/Elysia backends, React web frontends, and Expo native apps, all connected through tRPC. ## Quick Start @@ -23,6 +23,7 @@ Follow the prompts to configure your project or use the `-y` flag for defaults. - **Monorepo**: Turborepo for optimized build system and workspace management - **Frontend**: React, TanStack Router, TanStack Query, Tailwind CSS with shadcn/ui components +- **Native Apps**: Create React Native apps with Expo for iOS and Android - **Backend Frameworks**: Choose between Hono or Elysia - **API Layer**: End-to-end type safety with tRPC - **Runtime Options**: Choose between Bun or Node.js for your server @@ -70,6 +71,10 @@ Options: --hono Use Hono backend framework (default) --elysia Use Elysia backend framework --runtime Specify runtime (bun or node) + --web Include web frontend (default) + --native Include Expo frontend + --no-web Exclude web frontend + --no-native Exclude Expo frontend -h, --help Display help ``` diff --git a/apps/cli/src/constants.ts b/apps/cli/src/constants.ts index f7444b8..6fd897e 100644 --- a/apps/cli/src/constants.ts +++ b/apps/cli/src/constants.ts @@ -9,6 +9,7 @@ export const PKG_ROOT = path.join(distPath, "../"); export const DEFAULT_CONFIG: ProjectConfig = { projectName: "my-better-t-app", + frontend: ["web"], database: "sqlite", orm: "drizzle", auth: true, diff --git a/apps/cli/src/helpers/addons-setup.ts b/apps/cli/src/helpers/addons-setup.ts index 8d435eb..bba2912 100644 --- a/apps/cli/src/helpers/addons-setup.ts +++ b/apps/cli/src/helpers/addons-setup.ts @@ -87,7 +87,7 @@ async function setupPwa(projectDir: string) { await fs.copy(pwaTemplateDir, projectDir, { overwrite: true }); } - const clientPackageDir = path.join(projectDir, "apps/client"); + const clientPackageDir = path.join(projectDir, "apps/web"); addPackageDependency({ dependencies: ["vite-plugin-pwa"], diff --git a/apps/cli/src/helpers/auth-setup.ts b/apps/cli/src/helpers/auth-setup.ts index b365116..0de0215 100644 --- a/apps/cli/src/helpers/auth-setup.ts +++ b/apps/cli/src/helpers/auth-setup.ts @@ -23,7 +23,7 @@ export async function setupAuth( } const serverDir = path.join(projectDir, "apps/server"); - const clientDir = path.join(projectDir, "apps/client"); + const clientDir = path.join(projectDir, "apps/web"); try { addPackageDependency({ diff --git a/apps/cli/src/helpers/create-project.ts b/apps/cli/src/helpers/create-project.ts index abd846e..9d988c5 100644 --- a/apps/cli/src/helpers/create-project.ts +++ b/apps/cli/src/helpers/create-project.ts @@ -18,6 +18,7 @@ import { fixGitignoreFiles, setupAuthTemplate, setupBackendFramework, + setupFrontendTemplates, setupOrmTemplate, } from "./template-manager"; @@ -29,6 +30,8 @@ export async function createProject(options: ProjectConfig): Promise { await fs.ensureDir(projectDir); await copyBaseTemplate(projectDir); + await setupFrontendTemplates(projectDir, options.frontend); + await fixGitignoreFiles(projectDir); await setupBackendFramework(projectDir, options.backendFramework); @@ -89,6 +92,7 @@ export async function createProject(options: ProjectConfig): Promise { options.orm, options.addons, options.runtime, + options.frontend, ); return projectDir; diff --git a/apps/cli/src/helpers/create-readme.ts b/apps/cli/src/helpers/create-readme.ts index 2e3cb242..c1ec46c 100644 --- a/apps/cli/src/helpers/create-readme.ts +++ b/apps/cli/src/helpers/create-readme.ts @@ -57,7 +57,7 @@ Then, run the development server: ${packageManagerRunCmd} dev \`\`\` -Open [http://localhost:3001](http://localhost:3001) in your browser to see the client application. +Open [http://localhost:3001](http://localhost:3001) in your browser to see the web application. The API is running at [http://localhost:3000](http://localhost:3000). ## Project Structure @@ -65,7 +65,7 @@ The API is running at [http://localhost:3000](http://localhost:3000). \`\`\` ${projectName}/ ├── apps/ -│ ├── client/ # Frontend application (React, TanStack Router) +│ ├── web/ # Frontend application (React, TanStack Router) │ └── server/ # Backend API (Hono, tRPC) \`\`\` @@ -173,9 +173,9 @@ function generateScriptsList( orm: ProjectOrm, auth: boolean, ): string { - let scripts = `- \`${packageManagerRunCmd} dev\`: Start both client and server in development mode -- \`${packageManagerRunCmd} build\`: Build both client and server -- \`${packageManagerRunCmd} dev:client\`: Start only the client + let scripts = `- \`${packageManagerRunCmd} dev\`: Start both web and server in development mode +- \`${packageManagerRunCmd} build\`: Build both web and server +- \`${packageManagerRunCmd} dev:web\`: Start only the web application - \`${packageManagerRunCmd} dev:server\`: Start only the server - \`${packageManagerRunCmd} check-types\`: Check TypeScript types across all apps`; diff --git a/apps/cli/src/helpers/env-setup.ts b/apps/cli/src/helpers/env-setup.ts index 126454b..25e09d5 100644 --- a/apps/cli/src/helpers/env-setup.ts +++ b/apps/cli/src/helpers/env-setup.ts @@ -8,7 +8,6 @@ export async function setupEnvironmentVariables( options: ProjectConfig, ): Promise { const serverDir = path.join(projectDir, "apps/server"); - const clientDir = path.join(projectDir, "apps/client"); const envPath = path.join(serverDir, ".env"); let envContent = ""; @@ -49,16 +48,35 @@ export async function setupEnvironmentVariables( await fs.writeFile(envPath, envContent.trim()); - const clientEnvPath = path.join(clientDir, ".env"); - let clientEnvContent = ""; + if (options.frontend.includes("web")) { + const clientDir = path.join(projectDir, "apps/web"); + const clientEnvPath = path.join(clientDir, ".env"); + let clientEnvContent = ""; - if (await fs.pathExists(clientEnvPath)) { - clientEnvContent = await fs.readFile(clientEnvPath, "utf8"); + if (await fs.pathExists(clientEnvPath)) { + clientEnvContent = await fs.readFile(clientEnvPath, "utf8"); + } + + if (!clientEnvContent.includes("VITE_SERVER_URL")) { + clientEnvContent += "VITE_SERVER_URL=http://localhost:3000\n"; + } + + await fs.writeFile(clientEnvPath, clientEnvContent.trim()); } - if (!clientEnvContent.includes("VITE_SERVER_URL")) { - clientEnvContent += "VITE_SERVER_URL=http://localhost:3000\n"; - } + if (options.frontend.includes("native")) { + const nativeDir = path.join(projectDir, "apps/native"); + const nativeEnvPath = path.join(nativeDir, ".env"); + let nativeEnvContent = ""; - await fs.writeFile(clientEnvPath, clientEnvContent.trim()); + if (await fs.pathExists(nativeEnvPath)) { + nativeEnvContent = await fs.readFile(nativeEnvPath, "utf8"); + } + + if (!nativeEnvContent.includes("EXPO_PUBLIC_SERVER_URL")) { + nativeEnvContent += "EXPO_PUBLIC_SERVER_URL=http://localhost:3000\n"; + } + + await fs.writeFile(nativeEnvPath, nativeEnvContent.trim()); + } } diff --git a/apps/cli/src/helpers/examples-setup.ts b/apps/cli/src/helpers/examples-setup.ts index f32a31a..f9661d5 100644 --- a/apps/cli/src/helpers/examples-setup.ts +++ b/apps/cli/src/helpers/examples-setup.ts @@ -23,8 +23,8 @@ async function setupTodoExample( ): Promise { const todoExampleDir = path.join(PKG_ROOT, "template/examples/todo"); if (await fs.pathExists(todoExampleDir)) { - const todoRouteDir = path.join(todoExampleDir, "apps/client/src/routes"); - const targetRouteDir = path.join(projectDir, "apps/client/src/routes"); + const todoRouteDir = path.join(todoExampleDir, "apps/web/src/routes"); + const targetRouteDir = path.join(projectDir, "apps/web/src/routes"); await fs.copy(todoRouteDir, targetRouteDir, { overwrite: true }); if (orm !== "none") { @@ -55,7 +55,7 @@ async function updateHeaderWithTodoLink( ): Promise { const headerPath = path.join( projectDir, - "apps/client/src/components/header.tsx", + "apps/web/src/components/header.tsx", ); if (await fs.pathExists(headerPath)) { @@ -125,7 +125,7 @@ async function updateRouterIndex(projectDir: string): Promise { } async function addTodoButtonToHomepage(projectDir: string): Promise { - const indexPath = path.join(projectDir, "apps/client/src/routes/index.tsx"); + const indexPath = path.join(projectDir, "apps/web/src/routes/index.tsx"); if (await fs.pathExists(indexPath)) { let indexContent = await fs.readFile(indexPath, "utf8"); diff --git a/apps/cli/src/helpers/post-installation.ts b/apps/cli/src/helpers/post-installation.ts index 82d6bad..5453a5c 100644 --- a/apps/cli/src/helpers/post-installation.ts +++ b/apps/cli/src/helpers/post-installation.ts @@ -4,6 +4,7 @@ import type { PackageManager, ProjectAddons, ProjectDatabase, + ProjectFrontend, ProjectOrm, Runtime, } from "../types"; @@ -13,9 +14,10 @@ export function displayPostInstallInstructions( projectName: string, packageManager: PackageManager, depsInstalled: boolean, - orm?: ProjectOrm, - addons?: ProjectAddons[], - runtime?: Runtime, + orm: ProjectOrm, + addons: ProjectAddons[], + runtime: Runtime, + frontends: ProjectFrontend[], ) { const runCmd = packageManager === "npm" ? "npm run" : packageManager; const cdCmd = `cd ${projectName}`; @@ -32,15 +34,29 @@ export function displayPostInstallInstructions( const lintingInstructions = hasHuskyOrBiome ? getLintingInstructions(runCmd) : ""; + const nativeInstructions = frontends?.includes("native") + ? getNativeInstructions() + : ""; + + const hasWebFrontend = frontends?.includes("web"); + const hasNativeFrontend = frontends?.includes("native"); + const hasFrontend = hasWebFrontend || hasNativeFrontend; log.info(`${pc.bold("Next steps:")} ${pc.cyan("1.")} ${cdCmd} ${!depsInstalled ? `${pc.cyan("2.")} ${packageManager} install\n` : ""}${pc.cyan(depsInstalled ? "2." : "3.")} ${runCmd} dev ${pc.bold("Your project will be available at:")} -${pc.cyan("•")} Frontend: http://localhost:3001 -${pc.cyan("•")} API: http://localhost:3000 -${databaseInstructions ? `\n${databaseInstructions.trim()}` : ""}${tauriInstructions ? `\n${tauriInstructions.trim()}` : ""}${lintingInstructions ? `\n${lintingInstructions.trim()}` : ""}`); +${ + hasFrontend + ? `${hasWebFrontend ? `${pc.cyan("•")} Frontend: http://localhost:3001\n` : ""}` + : `${pc.yellow("NOTE:")} You are creating a backend-only app (no frontend selected)\n` +}${pc.cyan("•")} API: http://localhost:3000 +${nativeInstructions ? `\n${nativeInstructions.trim()}` : ""}${databaseInstructions ? `\n${databaseInstructions.trim()}` : ""}${tauriInstructions ? `\n${tauriInstructions.trim()}` : ""}${lintingInstructions ? `\n${lintingInstructions.trim()}` : ""}`); +} + +function getNativeInstructions(): string { + return `${pc.yellow("NOTE:")} If the Expo app cannot connect to the server, update the EXPO_PUBLIC_SERVER_URL in apps/native/.env to use your local IP address instead of localhost:\n${pc.dim("EXPO_PUBLIC_SERVER_URL=http://192.168.0.103:3000")}\n`; } function getLintingInstructions(runCmd?: string): string { @@ -95,5 +111,5 @@ function getDatabaseInstructions( } function getTauriInstructions(runCmd?: string): string { - return `${pc.bold("Desktop app with Tauri:")}\n${pc.cyan("•")} Start desktop app: ${pc.dim(`cd apps/client && ${runCmd} desktop:dev`)}\n${pc.cyan("•")} Build desktop app: ${pc.dim(`cd apps/client && ${runCmd} desktop:build`)}\n${pc.yellow("NOTE:")} Tauri requires Rust and platform-specific dependencies. See: ${pc.dim("https://v2.tauri.app/start/prerequisites/")}\n\n`; + return `${pc.bold("Desktop app with Tauri:")}\n${pc.cyan("•")} Start desktop app: ${pc.dim(`cd apps/web && ${runCmd} desktop:dev`)}\n${pc.cyan("•")} Build desktop app: ${pc.dim(`cd apps/web && ${runCmd} desktop:build`)}\n${pc.yellow("NOTE:")} Tauri requires Rust and platform-specific dependencies. See: ${pc.dim("https://v2.tauri.app/start/prerequisites/")}\n\n`; } diff --git a/apps/cli/src/helpers/tauri-setup.ts b/apps/cli/src/helpers/tauri-setup.ts index 0108bcc..f590792 100644 --- a/apps/cli/src/helpers/tauri-setup.ts +++ b/apps/cli/src/helpers/tauri-setup.ts @@ -11,7 +11,7 @@ export async function setupTauri( packageManager: PackageManager, ): Promise { const s = spinner(); - const clientPackageDir = path.join(projectDir, "apps/client"); + const clientPackageDir = path.join(projectDir, "apps/web"); try { s.start("Setting up Tauri desktop app support..."); diff --git a/apps/cli/src/helpers/template-manager.ts b/apps/cli/src/helpers/template-manager.ts index d4e7c96..44c1783 100644 --- a/apps/cli/src/helpers/template-manager.ts +++ b/apps/cli/src/helpers/template-manager.ts @@ -1,7 +1,12 @@ import path from "node:path"; import fs from "fs-extra"; import { PKG_ROOT } from "../constants"; -import type { BackendFramework, ProjectDatabase, ProjectOrm } from "../types"; +import type { + BackendFramework, + ProjectDatabase, + ProjectFrontend, + ProjectOrm, +} from "../types"; export async function copyBaseTemplate(projectDir: string): Promise { const templateDir = path.join(PKG_ROOT, "template/base"); @@ -11,6 +16,30 @@ export async function copyBaseTemplate(projectDir: string): Promise { await fs.copy(templateDir, projectDir); } +export async function setupFrontendTemplates( + projectDir: string, + frontends: ProjectFrontend[], +): Promise { + if (!frontends.includes("web")) { + const webDir = path.join(projectDir, "apps/web"); + if (await fs.pathExists(webDir)) { + await fs.remove(webDir); + } + } + + if (!frontends.includes("native")) { + const nativeDir = path.join(projectDir, "apps/native"); + if (await fs.pathExists(nativeDir)) { + await fs.remove(nativeDir); + } + } else { + await fs.writeFile( + path.join(projectDir, ".npmrc"), + "node-linker=hoisted\n", + ); + } +} + export async function setupBackendFramework( projectDir: string, framework: BackendFramework, @@ -67,8 +96,8 @@ export async function setupAuthTemplate( const authTemplateDir = path.join(PKG_ROOT, "template/with-auth"); if (await fs.pathExists(authTemplateDir)) { - const clientAuthDir = path.join(authTemplateDir, "apps/client"); - const projectClientDir = path.join(projectDir, "apps/client"); + const clientAuthDir = path.join(authTemplateDir, "apps/web"); + const projectClientDir = path.join(projectDir, "apps/web"); await fs.copy(clientAuthDir, projectClientDir, { overwrite: true }); const serverAuthDir = path.join(authTemplateDir, "apps/server/src"); @@ -118,7 +147,8 @@ export async function setupAuthTemplate( export async function fixGitignoreFiles(projectDir: string): Promise { const gitignorePaths = [ path.join(projectDir, "_gitignore"), - path.join(projectDir, "apps/client/_gitignore"), + path.join(projectDir, "apps/web/_gitignore"), + path.join(projectDir, "apps/native/_gitignore"), path.join(projectDir, "apps/server/_gitignore"), ]; diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts index e02dd72..c564181 100644 --- a/apps/cli/src/index.ts +++ b/apps/cli/src/index.ts @@ -10,6 +10,7 @@ import type { ProjectAddons, ProjectConfig, ProjectExamples, + ProjectFrontend, Runtime, } from "./types"; import { displayConfig } from "./utils/display-config"; @@ -59,6 +60,10 @@ async function main() { .option("--hono", "Use Hono backend framework") .option("--elysia", "Use Elysia backend framework") .option("--runtime ", "Specify runtime (bun or node)") + .option("--web", "Include web frontend") + .option("--native", "Include Expo frontend") + .option("--no-web", "Exclude web frontend") + .option("--no-native", "Exclude Expo frontend") .parse(); const s = spinner(); @@ -115,6 +120,16 @@ async function main() { .filter((e) => e === "todo") as ProjectExamples[]) : [], }), + ...((options.web !== undefined || options.native !== undefined) && { + frontend: [ + ...(options.web === false ? [] : options.web === true ? ["web"] : []), + ...(options.native === false + ? [] + : options.native === true + ? ["native"] + : []), + ] as ProjectFrontend[], + }), }; if (!options.yes && Object.keys(flagConfig).length > 0) { diff --git a/apps/cli/src/prompts/auth.ts b/apps/cli/src/prompts/auth.ts index bd270a7..65ff619 100644 --- a/apps/cli/src/prompts/auth.ts +++ b/apps/cli/src/prompts/auth.ts @@ -1,12 +1,26 @@ -import { cancel, confirm, isCancel } from "@clack/prompts"; +import { cancel, confirm, isCancel, log } from "@clack/prompts"; import pc from "picocolors"; import { DEFAULT_CONFIG } from "../constants"; +import type { ProjectFrontend } from "../types"; export async function getAuthChoice( auth: boolean | undefined, hasDatabase: boolean, + frontends?: ProjectFrontend[], ): Promise { if (!hasDatabase) return false; + + const hasNative = frontends?.includes("native"); + const hasWeb = frontends?.includes("web"); + + if (hasNative) { + log.warn( + pc.yellow("Note: Authentication is not yet available with native"), + ); + } + + if (!hasWeb) return false; + if (auth !== undefined) return auth; const response = await confirm({ diff --git a/apps/cli/src/prompts/config-prompts.ts b/apps/cli/src/prompts/config-prompts.ts index 6ff687f..a6706c6 100644 --- a/apps/cli/src/prompts/config-prompts.ts +++ b/apps/cli/src/prompts/config-prompts.ts @@ -7,6 +7,7 @@ import type { ProjectConfig, ProjectDatabase, ProjectExamples, + ProjectFrontend, ProjectOrm, Runtime, } from "../types"; @@ -15,6 +16,7 @@ import { getAuthChoice } from "./auth"; import { getBackendFrameworkChoice } from "./backend-framework"; import { getDatabaseChoice } from "./database"; import { getExamplesChoice } from "./examples"; +import { getFrontendChoice } from "./frontend-option"; import { getGitChoice } from "./git"; import { getNoInstallChoice } from "./install"; import { getORMChoice } from "./orm"; @@ -36,6 +38,7 @@ type PromptGroupResults = { turso: boolean; backendFramework: BackendFramework; runtime: Runtime; + frontend: ProjectFrontend[]; }; export async function gatherConfig( @@ -46,13 +49,18 @@ export async function gatherConfig( projectName: async () => { return getProjectName(flags.projectName); }, + frontend: () => getFrontendChoice(flags.frontend), backendFramework: () => getBackendFrameworkChoice(flags.backendFramework), runtime: () => getRuntimeChoice(flags.runtime), database: () => getDatabaseChoice(flags.database), orm: ({ results }) => getORMChoice(flags.orm, results.database !== "none"), auth: ({ results }) => - getAuthChoice(flags.auth, results.database !== "none"), + getAuthChoice( + flags.auth, + results.database !== "none", + results.frontend, + ), turso: ({ results }) => results.database === "sqlite" && results.orm !== "prisma" ? getTursoSetupChoice(flags.turso) @@ -74,6 +82,7 @@ export async function gatherConfig( return { projectName: result.projectName, + frontend: result.frontend, database: result.database, orm: result.orm, auth: result.auth, diff --git a/apps/cli/src/prompts/frontend-option.ts b/apps/cli/src/prompts/frontend-option.ts new file mode 100644 index 0000000..015a45c --- /dev/null +++ b/apps/cli/src/prompts/frontend-option.ts @@ -0,0 +1,35 @@ +import { cancel, isCancel, multiselect } from "@clack/prompts"; +import pc from "picocolors"; +import { DEFAULT_CONFIG } from "../constants"; +import type { ProjectFrontend } from "../types"; + +export async function getFrontendChoice( + frontendOptions?: ProjectFrontend[], +): Promise { + if (frontendOptions !== undefined) return frontendOptions; + + const response = await multiselect({ + message: "Which frontend applications would you like to create?", + options: [ + { + value: "web", + label: "Web App", + hint: "React + TanStack Router web application", + }, + { + value: "native", + label: "Native App", + hint: "React Native + Expo application", + }, + ], + initialValues: DEFAULT_CONFIG.frontend, + required: false, + }); + + if (isCancel(response)) { + cancel(pc.red("Operation cancelled")); + process.exit(0); + } + + return response; +} diff --git a/apps/cli/src/prompts/orm.ts b/apps/cli/src/prompts/orm.ts index 2661202..377856c 100644 --- a/apps/cli/src/prompts/orm.ts +++ b/apps/cli/src/prompts/orm.ts @@ -21,7 +21,7 @@ export async function getORMChoice( { value: "prisma", label: "Prisma", - hint: "Powerful, feature-rich ORM with schema migrations", + hint: "Powerful, feature-rich ORM", }, ], initialValue: DEFAULT_CONFIG.orm, diff --git a/apps/cli/src/types.ts b/apps/cli/src/types.ts index 23a2d37..64a39f6 100644 --- a/apps/cli/src/types.ts +++ b/apps/cli/src/types.ts @@ -5,6 +5,7 @@ export type ProjectAddons = "pwa" | "biome" | "tauri" | "husky"; export type BackendFramework = "hono" | "elysia"; export type Runtime = "node" | "bun"; export type ProjectExamples = "todo"; +export type ProjectFrontend = "web" | "native"; export interface ProjectConfig { projectName: string; @@ -19,4 +20,5 @@ export interface ProjectConfig { packageManager: PackageManager; noInstall?: boolean; turso?: boolean; + frontend: ProjectFrontend[]; } diff --git a/apps/cli/src/utils/generate-reproducible-command.ts b/apps/cli/src/utils/generate-reproducible-command.ts index 5cb95c4..1dc8ef8 100644 --- a/apps/cli/src/utils/generate-reproducible-command.ts +++ b/apps/cli/src/utils/generate-reproducible-command.ts @@ -35,6 +35,20 @@ export function generateReproducibleCommand(config: ProjectConfig): string { flags.push(`--runtime ${config.runtime}`); } + if (config.frontend) { + if (config.frontend.includes("web")) { + flags.push("--web"); + } else { + flags.push("--no-web"); + } + + if (config.frontend.includes("native")) { + flags.push("--native"); + } else { + flags.push("--no-native"); + } + } + if (config.addons.length > 0) { for (const addon of config.addons) { flags.push(`--${addon}`); diff --git a/apps/cli/template/base/apps/client/src/main.tsx b/apps/cli/template/base/apps/client/src/main.tsx deleted file mode 100644 index 71c12fd..0000000 --- a/apps/cli/template/base/apps/client/src/main.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { - QueryCache, - QueryClient, - QueryClientProvider, -} from "@tanstack/react-query"; -import { RouterProvider, createRouter } from "@tanstack/react-router"; -import { httpBatchLink } from "@trpc/client"; -import { createTRPCQueryUtils } from "@trpc/react-query"; -import ReactDOM from "react-dom/client"; -import { toast } from "sonner"; -import Loader from "./components/loader"; -import { routeTree } from "./routeTree.gen"; -import { trpc } from "./utils/trpc"; - -const queryClient = new QueryClient({ - queryCache: new QueryCache({ - onError: (error) => { - toast.error(error.message, { - action: { - label: "retry", - onClick: () => { - queryClient.invalidateQueries(); - }, - }, - }); - }, - }), -}); - -const trpcClient = trpc.createClient({ - links: [ - httpBatchLink({ - url: `${import.meta.env.VITE_SERVER_URL}/trpc`, - }), - ], -}); - -export const trpcQueryUtils = createTRPCQueryUtils({ - queryClient, - client: trpcClient, -}); - -const router = createRouter({ - routeTree, - defaultPreload: "intent", - context: { trpcQueryUtils }, - defaultPendingComponent: () => , - Wrap: function WrapComponent({ children }) { - return ( - - - {children} - - - ); - }, -}); - -// Register things for typesafety -declare module "@tanstack/react-router" { - interface Register { - router: typeof router; - } -} - -const rootElement = document.getElementById("app"); -if (!rootElement) throw new Error("Root element not found"); - -if (!rootElement.innerHTML) { - const root = ReactDOM.createRoot(rootElement); - root.render(); -} diff --git a/apps/cli/template/base/apps/client/src/utils/trpc.ts b/apps/cli/template/base/apps/client/src/utils/trpc.ts deleted file mode 100644 index 1bf0882..0000000 --- a/apps/cli/template/base/apps/client/src/utils/trpc.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { createTRPCReact } from "@trpc/react-query"; -import type { AppRouter } from "../../../server/src/routers"; - -export const trpc = createTRPCReact(); diff --git a/apps/cli/template/base/apps/native/_gitignore b/apps/cli/template/base/apps/native/_gitignore new file mode 100644 index 0000000..2ff55da --- /dev/null +++ b/apps/cli/template/base/apps/native/_gitignore @@ -0,0 +1,24 @@ +node_modules/ +.expo/ +dist/ +npm-debug.* +*.jks +*.p8 +*.p12 +*.key +*.mobileprovision +*.orig.* +web-build/ +# expo router +expo-env.d.ts + + + +ios +android + +# macOS +.DS_Store + +# Temporary files created by Metro to check the health of the file watcher +.metro-health-check* \ No newline at end of file diff --git a/apps/cli/template/base/apps/native/app-env.d.ts b/apps/cli/template/base/apps/native/app-env.d.ts new file mode 100644 index 0000000..88dc403 --- /dev/null +++ b/apps/cli/template/base/apps/native/app-env.d.ts @@ -0,0 +1,2 @@ +// @ts-ignore +/// diff --git a/apps/cli/template/base/apps/native/app.json b/apps/cli/template/base/apps/native/app.json new file mode 100644 index 0000000..cbb5b11 --- /dev/null +++ b/apps/cli/template/base/apps/native/app.json @@ -0,0 +1,38 @@ +{ + "expo": { + "name": "my-better-t-app", + "slug": "my-better-t-app", + "version": "1.0.0", + "scheme": "my-better-t-app", + "web": { + "bundler": "metro", + "output": "static", + "favicon": "./assets/favicon.png" + }, + "plugins": ["expo-router"], + "experiments": { + "typedRoutes": true, + "tsconfigPaths": true + }, + "orientation": "portrait", + "icon": "./assets/icon.png", + "userInterfaceStyle": "light", + "splash": { + "image": "./assets/splash.png", + "resizeMode": "contain", + "backgroundColor": "#ffffff" + }, + "assetBundlePatterns": ["**/*"], + "ios": { + "supportsTablet": true, + "bundleIdentifier": "com.amanvarshney01.mybettertapp" + }, + "android": { + "adaptiveIcon": { + "foregroundImage": "./assets/adaptive-icon.png", + "backgroundColor": "#ffffff" + }, + "package": "com.amanvarshney01.mybettertapp" + } + } +} diff --git a/apps/cli/template/base/apps/native/app/(drawer)/(tabs)/_layout.tsx b/apps/cli/template/base/apps/native/app/(drawer)/(tabs)/_layout.tsx new file mode 100644 index 0000000..852bbc9 --- /dev/null +++ b/apps/cli/template/base/apps/native/app/(drawer)/(tabs)/_layout.tsx @@ -0,0 +1,29 @@ +import { Tabs } from "expo-router"; + +import { TabBarIcon } from "@/components/tabbar-icon"; + +export default function TabLayout() { + return ( + + , + }} + /> + , + }} + /> + + ); +} diff --git a/apps/cli/template/base/apps/native/app/(drawer)/(tabs)/index.tsx b/apps/cli/template/base/apps/native/app/(drawer)/(tabs)/index.tsx new file mode 100644 index 0000000..e8ce549 --- /dev/null +++ b/apps/cli/template/base/apps/native/app/(drawer)/(tabs)/index.tsx @@ -0,0 +1,9 @@ +import { View, Text } from "react-native"; + +export default function App() { + return ( + + Hello, World! + + ); +} diff --git a/apps/cli/template/base/apps/native/app/(drawer)/(tabs)/two.tsx b/apps/cli/template/base/apps/native/app/(drawer)/(tabs)/two.tsx new file mode 100644 index 0000000..f1ab919 --- /dev/null +++ b/apps/cli/template/base/apps/native/app/(drawer)/(tabs)/two.tsx @@ -0,0 +1,16 @@ +import { Stack } from "expo-router"; +import { View, Text } from "react-native"; + +import { Container } from "@/components/container"; + +export default function Home() { + return ( + <> + + + Tab Two + + + + ); +} diff --git a/apps/cli/template/base/apps/native/app/(drawer)/_layout.tsx b/apps/cli/template/base/apps/native/app/(drawer)/_layout.tsx new file mode 100644 index 0000000..c1b5640 --- /dev/null +++ b/apps/cli/template/base/apps/native/app/(drawer)/_layout.tsx @@ -0,0 +1,39 @@ +import { Ionicons, MaterialIcons } from "@expo/vector-icons"; +import { Link } from "expo-router"; +import { Drawer } from "expo-router/drawer"; + +import { HeaderButton } from "@/components/header-button"; + +const DrawerLayout = () => { + return ( + + ( + + ), + }} + /> + ( + + ), + headerRight: () => ( + + + + ), + }} + /> + + ); +}; + +export default DrawerLayout; diff --git a/apps/cli/template/base/apps/native/app/(drawer)/index.tsx b/apps/cli/template/base/apps/native/app/(drawer)/index.tsx new file mode 100644 index 0000000..ee6c5d4 --- /dev/null +++ b/apps/cli/template/base/apps/native/app/(drawer)/index.tsx @@ -0,0 +1,36 @@ +import { useQuery } from "@tanstack/react-query"; +import { View, Text, ScrollView } from "react-native"; +import { Container } from "@/components/container"; +import { trpc } from "@/utils/trpc"; + +export default function Home() { + const healthCheck = useQuery(trpc.healthCheck.queryOptions()); + + return ( + + + + BETTER T STACK + + + + API Status + + + + {healthCheck.isLoading + ? "Checking..." + : healthCheck.data + ? "Connected" + : "Disconnected"} + + + + + + ); +} diff --git a/apps/cli/template/base/apps/native/app/+html.tsx b/apps/cli/template/base/apps/native/app/+html.tsx new file mode 100644 index 0000000..2fe2848 --- /dev/null +++ b/apps/cli/template/base/apps/native/app/+html.tsx @@ -0,0 +1,46 @@ +import { ScrollViewStyleReset } from 'expo-router/html'; + +// This file is web-only and used to configure the root HTML for every +// web page during static rendering. +// The contents of this function only run in Node.js environments and +// do not have access to the DOM or browser APIs. +export default function Root({ children }: { children: React.ReactNode }) { + return ( + + + + + + {/* + This viewport disables scaling which makes the mobile website act more like a native app. + However this does reduce built-in accessibility. If you want to enable scaling, use this instead: + + */} + + {/* + Disable body scrolling on web. This makes ScrollView components work closer to how they do on native. + However, body scrolling is often nice to have for mobile web. If you want to enable it, remove this line. + */} + + + {/* Using raw CSS styles as an escape-hatch to ensure the background color never flickers in dark-mode. */} +