Add Biome and Husky for code formatting and Git hooks

This commit is contained in:
Aman Varshney
2025-03-22 22:51:59 +05:30
parent 0790fd0894
commit 996b35b78a
37 changed files with 609 additions and 437 deletions

View File

@@ -0,0 +1,5 @@
---
"create-better-t-stack": minor
---
Add Biome and Husky for code formatting and Git hooks

14
.github/workflows/main.yaml vendored Normal file
View File

@@ -0,0 +1,14 @@
name: CI
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- run: bun install --frozen-lockfile
- run: bun run check

View File

@@ -1,32 +1,28 @@
name: Release name: Publish
on: on:
push: push:
branches: branches:
- main - main
paths: paths:
- 'packages/cli/**' - 'apps/cli/**'
- '.changeset/**' - '.changeset/**'
- 'package.json' - 'package.json'
- 'bun.lock' - 'bun.lock'
concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs: jobs:
release: publish:
name: Release
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - uses: actions/checkout@v4
uses: actions/checkout@v4 - uses: oven-sh/setup-bun@v2
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with: with:
bun-version: latest bun-version: latest
- run: bun install --frozen-lockfile
- name: Install Dependencies - name: Create Release Pull Request
run: bun install id: changesets
- name: Create Release Pull Request or Publish
uses: changesets/action@v1 uses: changesets/action@v1
with: with:
publish: bun run publish-packages publish: bun run publish-packages

View File

@@ -33,6 +33,11 @@ export const dependencyVersionMap = {
"@vite-pwa/assets-generator": "^0.2.6", "@vite-pwa/assets-generator": "^0.2.6",
"@tauri-apps/cli": "^2.4.0", "@tauri-apps/cli": "^2.4.0",
"@biomejs/biome": "1.9.4",
husky: "^9.1.7",
"lint-staged": "^15.5.0",
} as const; } as const;
export type AvailableDependencies = keyof typeof dependencyVersionMap; export type AvailableDependencies = keyof typeof dependencyVersionMap;

View File

@@ -19,6 +19,66 @@ export async function setupAddons(
if (addons.includes("tauri")) { if (addons.includes("tauri")) {
await setupTauri(projectDir, packageManager); await setupTauri(projectDir, packageManager);
} }
if (addons.includes("biome")) {
await setupBiome(projectDir);
}
if (addons.includes("husky")) {
await setupHusky(projectDir);
}
}
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({
devDependencies: ["@biomejs/biome"],
projectDir,
});
const packageJsonPath = path.join(projectDir, "package.json");
if (await fs.pathExists(packageJsonPath)) {
const packageJson = await fs.readJson(packageJsonPath);
packageJson.scripts = {
...packageJson.scripts,
check: "biome check --write .",
};
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
}
}
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({
devDependencies: ["husky", "lint-staged"],
projectDir,
});
const packageJsonPath = path.join(projectDir, "package.json");
if (await fs.pathExists(packageJsonPath)) {
const packageJson = await fs.readJson(packageJsonPath);
packageJson.scripts = {
...packageJson.scripts,
prepare: "husky",
};
packageJson["lint-staged"] = {
"*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}": [
"biome check --no-errors-on-unmatched --files-ignore-unknown=true",
],
};
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
}
} }
async function setupDocker(projectDir: string) { async function setupDocker(projectDir: string) {

View File

@@ -1,17 +1,18 @@
import { log, spinner } from "@clack/prompts"; import { log, spinner } from "@clack/prompts";
import { $ } from "execa"; import { $ } from "execa";
import pc from "picocolors"; import pc from "picocolors";
import type { ProjectAddons } from "../types";
import type { PackageManager } from "../utils/get-package-manager"; import type { PackageManager } from "../utils/get-package-manager";
interface InstallDependenciesOptions {
projectDir: string;
packageManager: PackageManager;
}
export async function installDependencies({ export async function installDependencies({
projectDir, projectDir,
packageManager, packageManager,
}: InstallDependenciesOptions) { addons = [],
}: {
projectDir: string;
packageManager: PackageManager;
addons?: ProjectAddons[];
}) {
const s = spinner(); const s = spinner();
try { try {
@@ -34,6 +35,11 @@ export async function installDependencies({
} }
s.stop("Dependencies installed successfully"); s.stop("Dependencies installed successfully");
// Run Biome check if Biome or Husky is enabled
if (addons.includes("biome") || addons.includes("husky")) {
await runBiomeCheck(projectDir, packageManager);
}
} catch (error) { } catch (error) {
s.stop(pc.red("Failed to install dependencies")); s.stop(pc.red("Failed to install dependencies"));
if (error instanceof Error) { if (error instanceof Error) {
@@ -42,3 +48,21 @@ export async function installDependencies({
throw error; throw error;
} }
} }
async function runBiomeCheck(
projectDir: string,
packageManager: PackageManager,
) {
const s = spinner();
try {
s.start("Running Biome format check...");
await $({ cwd: projectDir })`${packageManager} biome check --write .`;
s.stop("Biome check completed successfully");
} catch (error) {
s.stop(pc.yellow("Biome check encountered issues"));
log.warn(pc.yellow("Some files may need manual formatting"));
}
}

View File

@@ -17,6 +17,17 @@ export function displayPostInstallInstructions(
) { ) {
const runCmd = packageManager === "npm" ? "npm run" : packageManager; const runCmd = packageManager === "npm" ? "npm run" : packageManager;
const cdCmd = `cd ${projectName}`; const cdCmd = `cd ${projectName}`;
const hasHuskyOrBiome =
addons?.includes("husky") || addons?.includes("biome");
const databaseInstructions =
database !== "none" ? getDatabaseInstructions(database, orm, runCmd) : "";
const tauriInstructions = addons?.includes("tauri")
? getTauriInstructions(runCmd)
: "";
const lintingInstructions = hasHuskyOrBiome
? getLintingInstructions(runCmd)
: "";
log.info(`${pc.cyan("Project created successfully!")} log.info(`${pc.cyan("Project created successfully!")}
@@ -27,9 +38,11 @@ ${!depsInstalled ? `${pc.cyan("2.")} ${packageManager} install\n` : ""}${pc.cyan
${pc.bold("Your project will be available at:")} ${pc.bold("Your project will be available at:")}
${pc.cyan("•")} Frontend: http://localhost:3001 ${pc.cyan("•")} Frontend: http://localhost:3001
${pc.cyan("•")} API: http://localhost:3000 ${pc.cyan("•")} API: http://localhost:3000
${databaseInstructions ? `\n${databaseInstructions.trim()}` : ""}${tauriInstructions ? `\n${tauriInstructions.trim()}` : ""}${lintingInstructions ? `\n${lintingInstructions.trim()}` : ""}`);
}
${database !== "none" ? getDatabaseInstructions(database, orm, runCmd) : ""} function getLintingInstructions(runCmd?: string): string {
${addons?.includes("tauri") ? getTauriInstructions(runCmd) : ""}`); return `${pc.bold("Linting and formatting:")}\n${pc.cyan("•")} Format and lint fix: ${pc.dim(`${runCmd} check`)}\n\n`;
} }
function getDatabaseInstructions( function getDatabaseInstructions(

View File

@@ -33,6 +33,8 @@ async function main() {
.option("--docker", "Include Docker setup") .option("--docker", "Include Docker setup")
.option("--pwa", "Include Progressive Web App support") .option("--pwa", "Include Progressive Web App support")
.option("--tauri", "Include Tauri desktop app support") .option("--tauri", "Include Tauri desktop app support")
.option("--biome", "Include Biome for linting and formatting")
.option("--husky", "Include Husky, lint-staged for Git hooks")
.option("--no-addons", "Skip all additional addons") .option("--no-addons", "Skip all additional addons")
.option("--git", "Include git setup") .option("--git", "Include git setup")
.option("--no-git", "Skip git initialization") .option("--no-git", "Skip git initialization")
@@ -75,6 +77,8 @@ async function main() {
...((options.docker || ...((options.docker ||
options.pwa || options.pwa ||
options.tauri || options.tauri ||
options.biome ||
options.husky ||
options.addons === false) && { options.addons === false) && {
addons: addons:
options.addons === false options.addons === false
@@ -83,6 +87,8 @@ async function main() {
...(options.docker ? ["docker"] : []), ...(options.docker ? ["docker"] : []),
...(options.pwa ? ["pwa"] : []), ...(options.pwa ? ["pwa"] : []),
...(options.tauri ? ["tauri"] : []), ...(options.tauri ? ["tauri"] : []),
...(options.biome ? ["biome"] : []),
...(options.husky ? ["husky"] : []),
] as ProjectAddons[]), ] as ProjectAddons[]),
}), }),
}; };
@@ -141,6 +147,7 @@ async function main() {
await installDependencies({ await installDependencies({
projectDir, projectDir,
packageManager: config.packageManager, packageManager: config.packageManager,
addons: config.addons,
}); });
} }

View File

@@ -25,6 +25,16 @@ export async function getAddonsChoice(
label: "Tauri Desktop App", label: "Tauri Desktop App",
hint: "Build native desktop apps from your web frontend", hint: "Build native desktop apps from your web frontend",
}, },
{
value: "biome",
label: "Biome",
hint: "Add Biome for linting and formatting",
},
{
value: "husky",
label: "Husky",
hint: "Add Git hooks with Husky, lint-staged (requires Biome)",
},
], ],
required: false, required: false,
}); });
@@ -34,5 +44,9 @@ export async function getAddonsChoice(
process.exit(0); process.exit(0);
} }
if (response.includes("husky") && !response.includes("biome")) {
response.push("biome");
}
return response; return response;
} }

View File

@@ -1,7 +1,7 @@
export type ProjectDatabase = "sqlite" | "postgres" | "none"; export type ProjectDatabase = "sqlite" | "postgres" | "none";
export type ProjectOrm = "drizzle" | "prisma" | "none"; export type ProjectOrm = "drizzle" | "prisma" | "none";
export type PackageManager = "npm" | "pnpm" | "yarn" | "bun"; export type PackageManager = "npm" | "pnpm" | "yarn" | "bun";
export type ProjectAddons = "docker" | "pwa" | "tauri"; export type ProjectAddons = "docker" | "pwa" | "tauri" | "biome" | "husky";
export interface ProjectConfig { export interface ProjectConfig {
projectName: string; projectName: string;

View File

@@ -1,20 +1,18 @@
{ {
"name": "better-t-stack", "name": "better-t-stack",
"private": true, "private": true,
"workspaces": [ "workspaces": ["packages/*"],
"packages/*" "scripts": {
], "dev": "turbo dev",
"scripts": { "build": "turbo build",
"dev": "turbo dev", "check-types": "turbo check-types",
"build": "turbo build", "dev:client": "turbo -F client dev",
"check-types": "turbo check-types", "dev:server": "turbo -F server dev",
"dev:client": "turbo -F client dev", "db:push": "turbo -F server db:push",
"dev:server": "turbo -F server dev", "db:studio": "turbo -F server db:studio"
"db:push": "turbo -F server db:push", },
"db:studio": "turbo -F server db:studio" "packageManager": "bun@1.2.4",
}, "devDependencies": {
"packageManager": "bun@1.2.4", "turbo": "^2.4.2"
"devDependencies": { }
"turbo": "^2.4.2"
}
} }

View File

@@ -1,4 +0,0 @@
{
"plugins": ["prettier-plugin-tailwindcss"],
"tailwindStylesheet": "./src/index.css"
}

View File

@@ -1,51 +1,49 @@
{ {
"name": "client", "name": "client",
"version": "0.0.0", "version": "0.0.0",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite --port=3001", "dev": "vite --port=3001",
"build": "vite build", "build": "vite build",
"serve": "vite preview", "serve": "vite preview",
"start": "vite", "start": "vite",
"check-types": "tsc --noEmit" "check-types": "tsc --noEmit"
}, },
"devDependencies": { "devDependencies": {
"@tanstack/router-plugin": "^1.114.27", "@tanstack/router-plugin": "^1.114.27",
"@types/node": "^22.13.11", "@types/node": "^22.13.11",
"@types/react": "^19.0.12", "@types/react": "^19.0.12",
"@types/react-dom": "^19.0.4", "@types/react-dom": "^19.0.4",
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^4.3.4",
"postcss": "^8.5.3", "postcss": "^8.5.3",
"prettier": "^3.5.3", "tailwindcss": "^4.0.15",
"prettier-plugin-tailwindcss": "^0.6.11", "vite": "^6.2.2"
"tailwindcss": "^4.0.15", },
"vite": "^6.2.2" "dependencies": {
}, "@hookform/resolvers": "^3.10.0",
"dependencies": { "@radix-ui/react-checkbox": "^1.1.4",
"@hookform/resolvers": "^3.10.0", "@radix-ui/react-dropdown-menu": "^2.1.6",
"@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-label": "^2.1.2", "@tanstack/react-form": "^1.0.5",
"@radix-ui/react-slot": "^1.1.2", "@tailwindcss/vite": "^4.0.15",
"@tanstack/react-form": "^1.0.5", "@tanstack/react-query": "^5.69.0",
"@tailwindcss/vite": "^4.0.15", "@tanstack/react-query-devtools": "^5.69.0",
"@tanstack/react-query": "^5.69.0", "@tanstack/react-router": "^1.114.25",
"@tanstack/react-query-devtools": "^5.69.0", "@tanstack/router-devtools": "^1.114.25",
"@tanstack/react-router": "^1.114.25", "@trpc/client": "^11.0.0",
"@tanstack/router-devtools": "^1.114.25", "@trpc/react-query": "^11.0.0",
"@trpc/client": "^11.0.0", "@trpc/server": "^11.0.0",
"@trpc/react-query": "^11.0.0", "class-variance-authority": "^0.7.1",
"@trpc/server": "^11.0.0", "clsx": "^2.1.1",
"class-variance-authority": "^0.7.1", "lucide-react": "^0.473.0",
"clsx": "^2.1.1", "next-themes": "^0.4.6",
"lucide-react": "^0.473.0", "react": "^19.0.0",
"next-themes": "^0.4.6", "react-dom": "^19.0.0",
"react": "^19.0.0", "sonner": "^1.7.4",
"react-dom": "^19.0.0", "tailwind-merge": "^2.6.0",
"sonner": "^1.7.4", "tailwindcss-animate": "^1.0.7",
"tailwind-merge": "^2.6.0", "zod": "^3.24.2"
"tailwindcss-animate": "^1.0.7", }
"zod": "^3.24.2"
}
} }

View File

@@ -1,7 +1,7 @@
import { import {
QueryCache, QueryCache,
QueryClient, QueryClient,
QueryClientProvider, QueryClientProvider,
} from "@tanstack/react-query"; } from "@tanstack/react-query";
import { RouterProvider, createRouter } from "@tanstack/react-router"; import { RouterProvider, createRouter } from "@tanstack/react-router";
import { httpBatchLink } from "@trpc/client"; import { httpBatchLink } from "@trpc/client";
@@ -13,59 +13,60 @@ import { routeTree } from "./routeTree.gen";
import { trpc } from "./utils/trpc"; import { trpc } from "./utils/trpc";
const queryClient = new QueryClient({ const queryClient = new QueryClient({
queryCache: new QueryCache({ queryCache: new QueryCache({
onError: (error) => { onError: (error) => {
toast.error(error.message, { toast.error(error.message, {
action: { action: {
label: "retry", label: "retry",
onClick: () => { onClick: () => {
queryClient.invalidateQueries(); queryClient.invalidateQueries();
}, },
}, },
}); });
}, },
}), }),
}); });
const trpcClient = trpc.createClient({ const trpcClient = trpc.createClient({
links: [ links: [
httpBatchLink({ httpBatchLink({
url: `${import.meta.env.VITE_SERVER_URL}/trpc`, url: `${import.meta.env.VITE_SERVER_URL}/trpc`,
}), }),
], ],
}); });
export const trpcQueryUtils = createTRPCQueryUtils({ export const trpcQueryUtils = createTRPCQueryUtils({
queryClient, queryClient,
client: trpcClient, client: trpcClient,
}); });
const router = createRouter({ const router = createRouter({
routeTree, routeTree,
defaultPreload: "intent", defaultPreload: "intent",
context: { trpcQueryUtils }, context: { trpcQueryUtils },
defaultPendingComponent: () => <Loader />, defaultPendingComponent: () => <Loader />,
Wrap: function WrapComponent({ children }) { Wrap: function WrapComponent({ children }) {
return ( return (
<trpc.Provider client={trpcClient} queryClient={queryClient}> <trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
{children} {children}
</QueryClientProvider> </QueryClientProvider>
</trpc.Provider> </trpc.Provider>
); );
}, },
}); });
// Register things for typesafety // Register things for typesafety
declare module "@tanstack/react-router" { declare module "@tanstack/react-router" {
interface Register { interface Register {
router: typeof router; router: typeof router;
} }
} }
const rootElement = document.getElementById("app")!; const rootElement = document.getElementById("app");
if (!rootElement) throw new Error("Root element not found");
if (!rootElement.innerHTML) { if (!rootElement.innerHTML) {
const root = ReactDOM.createRoot(rootElement); const root = ReactDOM.createRoot(rootElement);
root.render(<RouterProvider router={router} />); root.render(<RouterProvider router={router} />);
} }

View File

@@ -33,9 +33,9 @@ function HomeComponent() {
<section className="rounded-lg border p-4"> <section className="rounded-lg border p-4">
<h2 className="mb-2 font-medium">API Status</h2> <h2 className="mb-2 font-medium">API Status</h2>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div <div
className={`h-2 w-2 rounded-full ${healthCheck.data ? "bg-green-500" : "bg-red-500"}`} className={`h-2 w-2 rounded-full ${healthCheck.data ? "bg-green-500" : "bg-red-500"}`}
></div> />
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
{healthCheck.isLoading {healthCheck.isLoading
? "Checking..." ? "Checking..."

View File

@@ -1,7 +1,7 @@
import tailwindcss from "@tailwindcss/vite"; import tailwindcss from "@tailwindcss/vite";
import { TanStackRouterVite } from "@tanstack/router-plugin/vite"; import { TanStackRouterVite } from "@tanstack/router-plugin/vite";
import react from "@vitejs/plugin-react"; import react from "@vitejs/plugin-react";
import path from "path"; import path from "node:path";
import { defineConfig } from "vite"; import { defineConfig } from "vite";
export default defineConfig({ export default defineConfig({

View File

@@ -1,25 +1,25 @@
{ {
"name": "server", "name": "server",
"main": "src/index.ts", "main": "src/index.ts",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "tsx watch src/index.ts", "dev": "tsx watch src/index.ts",
"build": "tsc", "build": "tsc",
"dev:bun": "bun run --hot src/index.ts", "dev:bun": "bun run --hot src/index.ts",
"check-types": "tsc --noEmit", "check-types": "tsc --noEmit",
"compile": "bun build --compile --minify --sourcemap --bytecode ./src/index.ts --outfile server" "compile": "bun build --compile --minify --sourcemap --bytecode ./src/index.ts --outfile server"
}, },
"dependencies": { "dependencies": {
"@hono/node-server": "^1.13.8", "@hono/node-server": "^1.13.8",
"@hono/trpc-server": "^0.3.4", "@hono/trpc-server": "^0.3.4",
"@trpc/server": "^11.0.0", "@trpc/server": "^11.0.0",
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"hono": "^4.7.5", "hono": "^4.7.5",
"zod": "^3.24.2" "zod": "^3.24.2"
}, },
"devDependencies": { "devDependencies": {
"tsx": "^4.19.2", "tsx": "^4.19.2",
"@types/node": "^22.13.11", "@types/node": "^22.13.11",
"typescript": "^5.8.2" "typescript": "^5.8.2"
} }
} }

View File

@@ -14,7 +14,7 @@ app.use(logger());
app.use( app.use(
"/*", "/*",
cors({ cors({
origin: process.env.CORS_ORIGIN!, origin: process.env.CORS_ORIGIN || "",
allowMethods: ["GET", "POST", "OPTIONS"], allowMethods: ["GET", "POST", "OPTIONS"],
}), }),
); );

View File

@@ -1,27 +1,27 @@
{ {
"$schema": "https://turbo.build/schema.json", "$schema": "https://turbo.build/schema.json",
"ui": "tui", "ui": "tui",
"tasks": { "tasks": {
"build": { "build": {
"dependsOn": ["^build"], "dependsOn": ["^build"],
"inputs": ["$TURBO_DEFAULT$", ".env*"], "inputs": ["$TURBO_DEFAULT$", ".env*"],
"outputs": ["dist/**"] "outputs": ["dist/**"]
}, },
"lint": { "lint": {
"dependsOn": ["^lint"] "dependsOn": ["^lint"]
}, },
"check-types": { "check": {
"dependsOn": ["^check-types"] "dependsOn": ["^check-types"]
}, },
"dev": { "dev": {
"cache": false, "cache": false,
"persistent": true "persistent": true
}, },
"db:push": { "db:push": {
"cache": false "cache": false
}, },
"db:studio": { "db:studio": {
"cache": false "cache": false
} }
} }
} }

View File

@@ -9,133 +9,131 @@ import { Input } from "./ui/input";
import { Label } from "./ui/label"; import { Label } from "./ui/label";
export default function SignInForm({ export default function SignInForm({
onSwitchToSignUp, onSwitchToSignUp,
}: { }: {
onSwitchToSignUp: () => void; onSwitchToSignUp: () => void;
}) { }) {
const navigate = useNavigate({ const navigate = useNavigate({
from: "/", from: "/",
}); });
const { isPending } = authClient.useSession(); const { isPending } = authClient.useSession();
const form = useForm({ const form = useForm({
defaultValues: { defaultValues: {
email: "", email: "",
password: "", password: "",
}, },
onSubmit: async ({ value }) => { onSubmit: async ({ value }) => {
await authClient.signIn.email( await authClient.signIn.email(
{ {
email: value.email, email: value.email,
password: value.password, password: value.password,
}, },
{ {
onSuccess: () => { onSuccess: () => {
toast.success("Sign in successful"); toast.success("Sign in successful");
navigate({ navigate({
to: "/dashboard", to: "/dashboard",
}); });
}, },
onError: (error) => { onError: (error) => {
toast.error(error.error.message); toast.error(error.error.message);
}, },
}, },
); );
}, },
validators: { validators: {
onSubmit: z.object({ onSubmit: z.object({
email: z.string().email("Invalid email address"), email: z.string().email("Invalid email address"),
password: z.string().min(6, "Password must be at least 6 characters"), password: z.string().min(6, "Password must be at least 6 characters"),
}), }),
}, },
}); });
if (isPending) { if (isPending) {
return <Loader />; return <Loader />;
} }
return ( return (
<div className="mx-auto mt-10 max-w-md p-6"> <div className="mx-auto mt-10 max-w-md p-6">
<h1 className="mb-6 text-center text-3xl font-bold">Welcome Back</h1> <h1 className="mb-6 text-center text-3xl font-bold">Welcome Back</h1>
<form <form
onSubmit={(e) => { onSubmit={(e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
void form.handleSubmit(); void form.handleSubmit();
}} }}
className="space-y-4" className="space-y-4"
> >
<div> <div>
<form.Field <form.Field name="email">
name="email" {(field) => (
children={(field) => ( <div className="space-y-2">
<div className="space-y-2"> <Label htmlFor={field.name}>Email</Label>
<Label htmlFor={field.name}>Email</Label> <Input
<Input id={field.name}
id={field.name} name={field.name}
name={field.name} type="email"
type="email" value={field.state.value}
value={field.state.value} onBlur={field.handleBlur}
onBlur={field.handleBlur} onChange={(e) => field.handleChange(e.target.value)}
onChange={(e) => field.handleChange(e.target.value)} />
/> {field.state.meta.errors.map((error) => (
{field.state.meta.errors.map((error) => ( <p key={error?.message} className="text-red-500">
<p key={error?.message} className="text-red-500"> {error?.message}
{error?.message} </p>
</p> ))}
))} </div>
</div> )}
)} </form.Field>
/> </div>
</div>
<div> <div>
<form.Field <form.Field name="password">
name="password" {(field) => (
children={(field) => ( <div className="space-y-2">
<div className="space-y-2"> <Label htmlFor={field.name}>Password</Label>
<Label htmlFor={field.name}>Password</Label> <Input
<Input id={field.name}
id={field.name} name={field.name}
name={field.name} type="password"
type="password" value={field.state.value}
value={field.state.value} onBlur={field.handleBlur}
onBlur={field.handleBlur} onChange={(e) => field.handleChange(e.target.value)}
onChange={(e) => field.handleChange(e.target.value)} />
/> {field.state.meta.errors.map((error) => (
{field.state.meta.errors.map((error) => ( <p key={error?.message} className="text-red-500">
<p key={error?.message} className="text-red-500"> {error?.message}
{error?.message} </p>
</p> ))}
))} </div>
</div> )}
)} </form.Field>
/> </div>
</div>
<form.Subscribe> <form.Subscribe>
{(state) => ( {(state) => (
<Button <Button
type="submit" type="submit"
className="w-full" className="w-full"
disabled={!state.canSubmit || state.isSubmitting} disabled={!state.canSubmit || state.isSubmitting}
> >
{state.isSubmitting ? "Submitting..." : "Sign In"} {state.isSubmitting ? "Submitting..." : "Sign In"}
</Button> </Button>
)} )}
</form.Subscribe> </form.Subscribe>
</form> </form>
<div className="mt-4 text-center"> <div className="mt-4 text-center">
<Button <Button
variant="link" variant="link"
onClick={onSwitchToSignUp} onClick={onSwitchToSignUp}
className="text-indigo-600 hover:text-indigo-800" className="text-indigo-600 hover:text-indigo-800"
> >
Need an account? Sign Up Need an account? Sign Up
</Button> </Button>
</div> </div>
</div> </div>
); );
} }

View File

@@ -1,7 +1,7 @@
import { import {
QueryCache, QueryCache,
QueryClient, QueryClient,
QueryClientProvider, QueryClientProvider,
} from "@tanstack/react-query"; } from "@tanstack/react-query";
import { RouterProvider, createRouter } from "@tanstack/react-router"; import { RouterProvider, createRouter } from "@tanstack/react-router";
import { httpBatchLink } from "@trpc/client"; import { httpBatchLink } from "@trpc/client";
@@ -13,65 +13,66 @@ import { routeTree } from "./routeTree.gen";
import { trpc } from "./utils/trpc"; import { trpc } from "./utils/trpc";
const queryClient = new QueryClient({ const queryClient = new QueryClient({
queryCache: new QueryCache({ queryCache: new QueryCache({
onError: (error) => { onError: (error) => {
toast.error(error.message, { toast.error(error.message, {
action: { action: {
label: "retry", label: "retry",
onClick: () => { onClick: () => {
queryClient.invalidateQueries(); queryClient.invalidateQueries();
}, },
}, },
}); });
}, },
}), }),
}); });
const trpcClient = trpc.createClient({ const trpcClient = trpc.createClient({
links: [ links: [
httpBatchLink({ httpBatchLink({
url: `${import.meta.env.VITE_SERVER_URL}/trpc`, url: `${import.meta.env.VITE_SERVER_URL}/trpc`,
fetch(url, options) { fetch(url, options) {
return fetch(url, { return fetch(url, {
...options, ...options,
credentials: "include", credentials: "include",
}); });
}, },
}), }),
], ],
}); });
export const trpcQueryUtils = createTRPCQueryUtils({ export const trpcQueryUtils = createTRPCQueryUtils({
queryClient, queryClient,
client: trpcClient, client: trpcClient,
}); });
const router = createRouter({ const router = createRouter({
routeTree, routeTree,
defaultPreload: "intent", defaultPreload: "intent",
context: { trpcQueryUtils }, context: { trpcQueryUtils },
defaultPendingComponent: () => <Loader />, defaultPendingComponent: () => <Loader />,
Wrap: function WrapComponent({ children }) { Wrap: function WrapComponent({ children }) {
return ( return (
<trpc.Provider client={trpcClient} queryClient={queryClient}> <trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
{children} {children}
</QueryClientProvider> </QueryClientProvider>
</trpc.Provider> </trpc.Provider>
); );
}, },
}); });
// Register things for typesafety // Register things for typesafety
declare module "@tanstack/react-router" { declare module "@tanstack/react-router" {
interface Register { interface Register {
router: typeof router; router: typeof router;
} }
} }
const rootElement = document.getElementById("app")!; const rootElement = document.getElementById("app");
if (!rootElement) throw new Error("Root element not found");
if (!rootElement.innerHTML) { if (!rootElement.innerHTML) {
const root = ReactDOM.createRoot(rootElement); const root = ReactDOM.createRoot(rootElement);
root.render(<RouterProvider router={router} />); root.render(<RouterProvider router={router} />);
} }

View File

@@ -1,10 +1,10 @@
import { trpc } from "@/utils/trpc";
import { createFileRoute, Link } from "@tanstack/react-router";
import { ArrowRight } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { trpc } from "@/utils/trpc";
import { Link, createFileRoute } from "@tanstack/react-router";
import { ArrowRight } from "lucide-react";
export const Route = createFileRoute("/")({ export const Route = createFileRoute("/")({
component: HomeComponent, component: HomeComponent,
}); });
const TITLE_TEXT = ` const TITLE_TEXT = `
@@ -24,74 +24,74 @@ const TITLE_TEXT = `
`; `;
function HomeComponent() { function HomeComponent() {
const healthCheck = trpc.healthCheck.useQuery(); const healthCheck = trpc.healthCheck.useQuery();
return ( return (
<div className="container mx-auto max-w-3xl px-4 py-2"> <div className="container mx-auto max-w-3xl px-4 py-2">
<pre className="overflow-x-auto font-mono text-sm">{TITLE_TEXT}</pre> <pre className="overflow-x-auto font-mono text-sm">{TITLE_TEXT}</pre>
<div className="grid gap-6"> <div className="grid gap-6">
<section className="rounded-lg border p-4"> <section className="rounded-lg border p-4">
<h2 className="mb-2 font-medium">API Status</h2> <h2 className="mb-2 font-medium">API Status</h2>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div <div
className={`h-2 w-2 rounded-full ${healthCheck.data ? "bg-green-500" : "bg-red-500"}`} className={`h-2 w-2 rounded-full ${healthCheck.data ? "bg-green-500" : "bg-red-500"}`}
></div> />
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
{healthCheck.isLoading {healthCheck.isLoading
? "Checking..." ? "Checking..."
: healthCheck.data : healthCheck.data
? "Connected" ? "Connected"
: "Disconnected"} : "Disconnected"}
</span> </span>
</div> </div>
</section> </section>
<section> <section>
<h2 className="mb-3 font-medium">Core Features</h2> <h2 className="mb-3 font-medium">Core Features</h2>
<ul className="grid grid-cols-2 gap-3"> <ul className="grid grid-cols-2 gap-3">
<FeatureItem <FeatureItem
title="Type-Safe API" title="Type-Safe API"
description="End-to-end type safety with tRPC" description="End-to-end type safety with tRPC"
/> />
<FeatureItem <FeatureItem
title="Modern React" title="Modern React"
description="TanStack Router + TanStack Query" description="TanStack Router + TanStack Query"
/> />
<FeatureItem <FeatureItem
title="Fast Backend" title="Fast Backend"
description="Lightweight Hono server" description="Lightweight Hono server"
/> />
<FeatureItem <FeatureItem
title="Beautiful UI" title="Beautiful UI"
description="TailwindCSS + shadcn/ui components" description="TailwindCSS + shadcn/ui components"
/> />
</ul> </ul>
</section> </section>
<div className="mt-4 flex flex-col gap-4 sm:flex-row sm:items-center"> <div className="mt-4 flex flex-col gap-4 sm:flex-row sm:items-center">
<Button asChild> <Button asChild>
<Link to="/todos" className="flex items-center"> <Link to="/todos" className="flex items-center">
View Todo Demo View Todo Demo
<ArrowRight className="ml-1 h-4 w-4" /> <ArrowRight className="ml-1 h-4 w-4" />
</Link> </Link>
</Button> </Button>
</div> </div>
</div> </div>
</div> </div>
); );
} }
function FeatureItem({ function FeatureItem({
title, title,
description, description,
}: { }: {
title: string; title: string;
description: string; description: string;
}) { }) {
return ( return (
<li className="border-l-2 border-primary py-1 pl-3"> <li className="border-l-2 border-primary py-1 pl-3">
<h3 className="font-medium">{title}</h3> <h3 className="font-medium">{title}</h3>
<p className="text-sm text-muted-foreground">{description}</p> <p className="text-sm text-muted-foreground">{description}</p>
</li> </li>
); );
} }

View File

@@ -15,7 +15,7 @@ app.use(logger());
app.use( app.use(
"/*", "/*",
cors({ cors({
origin: process.env.CORS_ORIGIN!, origin: process.env.CORS_ORIGIN || "",
allowMethods: ["GET", "POST", "OPTIONS"], allowMethods: ["GET", "POST", "OPTIONS"],
allowHeaders: ["Content-Type", "Authorization"], allowHeaders: ["Content-Type", "Authorization"],
credentials: true, credentials: true,

View File

@@ -0,0 +1,42 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"vcs": {
"enabled": false,
"clientKind": "git",
"useIgnoreFile": false
},
"files": {
"ignoreUnknown": false,
"ignore": [
".next",
"dist",
".turbo",
"dev-dist",
".zed",
".vscode",
"routeTree.gen.ts",
"src-tauri"
]
},
"formatter": {
"enabled": true,
"indentStyle": "tab"
},
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"correctness": {
"useExhaustiveDependencies": "info"
}
}
},
"javascript": {
"formatter": {
"quoteStyle": "double"
}
}
}

View File

@@ -5,6 +5,6 @@ export default defineConfig({
out: "./migrations", out: "./migrations",
dialect: "postgresql", dialect: "postgresql",
dbCredentials: { dbCredentials: {
url: process.env.POSTGRES_URL!, url: process.env.POSTGRES_URL || "",
}, },
}); });

View File

@@ -8,7 +8,7 @@ export const auth = betterAuth({
provider: "pg", provider: "pg",
schema: schema, schema: schema,
}), }),
trustedOrigins: [process.env.CORS_ORIGIN!], trustedOrigins: [process.env.CORS_ORIGIN || ""],
emailAndPassword: { emailAndPassword: {
enabled: true, enabled: true,
}, },

View File

@@ -5,7 +5,7 @@ export default defineConfig({
out: "./migrations", out: "./migrations",
dialect: "turso", dialect: "turso",
dbCredentials: { dbCredentials: {
url: process.env.TURSO_CONNECTION_URL!, url: process.env.TURSO_CONNECTION_URL || "",
authToken: process.env.TURSO_AUTH_TOKEN, authToken: process.env.TURSO_AUTH_TOKEN,
}, },
}); });

View File

@@ -2,7 +2,7 @@ import { drizzle } from "drizzle-orm/libsql";
export const db = drizzle({ export const db = drizzle({
connection: { connection: {
url: process.env.TURSO_CONNECTION_URL!, url: process.env.TURSO_CONNECTION_URL || "",
authToken: process.env.TURSO_AUTH_TOKEN, authToken: process.env.TURSO_AUTH_TOKEN,
}, },
// logger: true, // logger: true,

View File

@@ -8,7 +8,7 @@ export const auth = betterAuth({
provider: "sqlite", provider: "sqlite",
schema: schema, schema: schema,
}), }),
trustedOrigins: [process.env.CORS_ORIGIN!], trustedOrigins: [process.env.CORS_ORIGIN || ""],
emailAndPassword: { emailAndPassword: {
enabled: true, enabled: true,
}, },

View File

@@ -0,0 +1 @@
lint-staged

View File

@@ -6,7 +6,7 @@ export const auth = betterAuth({
database: prismaAdapter(prisma, { database: prismaAdapter(prisma, {
provider: "pg", provider: "pg",
}), }),
trustedOrigins: [process.env.CORS_ORIGIN!], trustedOrigins: [process.env.CORS_ORIGIN || ""],
emailAndPassword: { enabled: true }, emailAndPassword: { enabled: true },
advanced: { advanced: {
defaultCookieAttributes: { defaultCookieAttributes: {

View File

@@ -6,7 +6,7 @@ export const auth = betterAuth({
database: prismaAdapter(prisma, { database: prismaAdapter(prisma, {
provider: "sqlite", provider: "sqlite",
}), }),
trustedOrigins: [process.env.CORS_ORIGIN!], trustedOrigins: [process.env.CORS_ORIGIN || ""],
emailAndPassword: { enabled: true }, emailAndPassword: { enabled: true },
advanced: { advanced: {
defaultCookieAttributes: { defaultCookieAttributes: {

View File

@@ -1,7 +1,7 @@
import tailwindcss from "@tailwindcss/vite"; import tailwindcss from "@tailwindcss/vite";
import { TanStackRouterVite } from "@tanstack/router-plugin/vite"; import { TanStackRouterVite } from "@tanstack/router-plugin/vite";
import react from "@vitejs/plugin-react"; import react from "@vitejs/plugin-react";
import path from "path"; import path from "node:path";
import { defineConfig } from "vite"; import { defineConfig } from "vite";
import { VitePWA } from "vite-plugin-pwa"; import { VitePWA } from "vite-plugin-pwa";

View File

@@ -7,7 +7,7 @@
}, },
"files": { "files": {
"ignoreUnknown": false, "ignoreUnknown": false,
"ignore": [".next", "dist", ".source", "out", "template"] "ignore": [".next", "dist", ".source", "out", "template", ".turbo"]
}, },
"formatter": { "formatter": {
"enabled": true, "enabled": true,

View File

@@ -14,7 +14,7 @@
}, },
"apps/cli": { "apps/cli": {
"name": "create-better-t-stack", "name": "create-better-t-stack",
"version": "0.11.1", "version": "0.16.0",
"bin": { "bin": {
"create-better-t-stack": "dist/index.js", "create-better-t-stack": "dist/index.js",
}, },

View File

@@ -11,8 +11,7 @@
"build:web:cloudflare": "bun install && bun run build:web", "build:web:cloudflare": "bun install && bun run build:web",
"build:cli": "turbo run build --filter=create-better-t-stack", "build:cli": "turbo run build --filter=create-better-t-stack",
"check": "turbo check", "check": "turbo check",
"prepare": "husky", "publish-packages": "turbo run build --filter=create-better-t-stack && changeset publish"
"publish-packages": "turbo run build && changeset publish"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "1.9.4", "@biomejs/biome": "1.9.4",
@@ -28,6 +27,6 @@
"engines": { "engines": {
"node": ">=20" "node": ">=20"
}, },
"packageManager": "bun@1.2.2", "packageManager": "bun@1.2.5",
"workspaces": ["apps/*"] "workspaces": ["apps/*"]
} }

View File

@@ -10,8 +10,8 @@
"lint": { "lint": {
"dependsOn": ["^lint"] "dependsOn": ["^lint"]
}, },
"check-types": { "check": {
"dependsOn": ["^check-types"] "dependsOn": ["^check"]
}, },
"dev": { "dev": {
"cache": false, "cache": false,