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

View File

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

View File

@@ -19,6 +19,66 @@ export async function setupAddons(
if (addons.includes("tauri")) {
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) {

View File

@@ -1,17 +1,18 @@
import { log, spinner } from "@clack/prompts";
import { $ } from "execa";
import pc from "picocolors";
import type { ProjectAddons } from "../types";
import type { PackageManager } from "../utils/get-package-manager";
interface InstallDependenciesOptions {
projectDir: string;
packageManager: PackageManager;
}
export async function installDependencies({
projectDir,
packageManager,
}: InstallDependenciesOptions) {
addons = [],
}: {
projectDir: string;
packageManager: PackageManager;
addons?: ProjectAddons[];
}) {
const s = spinner();
try {
@@ -34,6 +35,11 @@ export async function installDependencies({
}
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) {
s.stop(pc.red("Failed to install dependencies"));
if (error instanceof Error) {
@@ -42,3 +48,21 @@ export async function installDependencies({
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 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!")}
@@ -27,9 +38,11 @@ ${!depsInstalled ? `${pc.cyan("2.")} ${packageManager} install\n` : ""}${pc.cyan
${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()}` : ""}`);
}
${database !== "none" ? getDatabaseInstructions(database, orm, runCmd) : ""}
${addons?.includes("tauri") ? getTauriInstructions(runCmd) : ""}`);
function getLintingInstructions(runCmd?: string): string {
return `${pc.bold("Linting and formatting:")}\n${pc.cyan("•")} Format and lint fix: ${pc.dim(`${runCmd} check`)}\n\n`;
}
function getDatabaseInstructions(

View File

@@ -33,6 +33,8 @@ async function main() {
.option("--docker", "Include Docker setup")
.option("--pwa", "Include Progressive Web 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("--git", "Include git setup")
.option("--no-git", "Skip git initialization")
@@ -75,6 +77,8 @@ async function main() {
...((options.docker ||
options.pwa ||
options.tauri ||
options.biome ||
options.husky ||
options.addons === false) && {
addons:
options.addons === false
@@ -83,6 +87,8 @@ async function main() {
...(options.docker ? ["docker"] : []),
...(options.pwa ? ["pwa"] : []),
...(options.tauri ? ["tauri"] : []),
...(options.biome ? ["biome"] : []),
...(options.husky ? ["husky"] : []),
] as ProjectAddons[]),
}),
};
@@ -141,6 +147,7 @@ async function main() {
await installDependencies({
projectDir,
packageManager: config.packageManager,
addons: config.addons,
});
}

View File

@@ -25,6 +25,16 @@ export async function getAddonsChoice(
label: "Tauri Desktop App",
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,
});
@@ -34,5 +44,9 @@ export async function getAddonsChoice(
process.exit(0);
}
if (response.includes("husky") && !response.includes("biome")) {
response.push("biome");
}
return response;
}

View File

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

View File

@@ -1,9 +1,7 @@
{
"name": "better-t-stack",
"private": true,
"workspaces": [
"packages/*"
],
"workspaces": ["packages/*"],
"scripts": {
"dev": "turbo dev",
"build": "turbo build",

View File

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

View File

@@ -17,8 +17,6 @@
"@types/react-dom": "^19.0.4",
"@vitejs/plugin-react": "^4.3.4",
"postcss": "^8.5.3",
"prettier": "^3.5.3",
"prettier-plugin-tailwindcss": "^0.6.11",
"tailwindcss": "^4.0.15",
"vite": "^6.2.2"
},

View File

@@ -63,7 +63,8 @@ declare module "@tanstack/react-router" {
}
}
const rootElement = document.getElementById("app")!;
const rootElement = document.getElementById("app");
if (!rootElement) throw new Error("Root element not found");
if (!rootElement.innerHTML) {
const root = ReactDOM.createRoot(rootElement);

View File

@@ -35,7 +35,7 @@ function HomeComponent() {
<div className="flex items-center gap-2">
<div
className={`h-2 w-2 rounded-full ${healthCheck.data ? "bg-green-500" : "bg-red-500"}`}
></div>
/>
<span className="text-sm text-muted-foreground">
{healthCheck.isLoading
? "Checking..."

View File

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

View File

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

View File

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

View File

@@ -67,9 +67,8 @@ export default function SignInForm({
className="space-y-4"
>
<div>
<form.Field
name="email"
children={(field) => (
<form.Field name="email">
{(field) => (
<div className="space-y-2">
<Label htmlFor={field.name}>Email</Label>
<Input
@@ -87,13 +86,12 @@ export default function SignInForm({
))}
</div>
)}
/>
</form.Field>
</div>
<div>
<form.Field
name="password"
children={(field) => (
<form.Field name="password">
{(field) => (
<div className="space-y-2">
<Label htmlFor={field.name}>Password</Label>
<Input
@@ -111,7 +109,7 @@ export default function SignInForm({
))}
</div>
)}
/>
</form.Field>
</div>
<form.Subscribe>

View File

@@ -69,7 +69,8 @@ declare module "@tanstack/react-router" {
}
}
const rootElement = document.getElementById("app")!;
const rootElement = document.getElementById("app");
if (!rootElement) throw new Error("Root element not found");
if (!rootElement.innerHTML) {
const root = ReactDOM.createRoot(rootElement);

View File

@@ -1,7 +1,7 @@
import { trpc } from "@/utils/trpc";
import { createFileRoute, Link } from "@tanstack/react-router";
import { ArrowRight } from "lucide-react";
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("/")({
component: HomeComponent,
@@ -35,7 +35,7 @@ function HomeComponent() {
<div className="flex items-center gap-2">
<div
className={`h-2 w-2 rounded-full ${healthCheck.data ? "bg-green-500" : "bg-red-500"}`}
></div>
/>
<span className="text-sm text-muted-foreground">
{healthCheck.isLoading
? "Checking..."

View File

@@ -15,7 +15,7 @@ app.use(logger());
app.use(
"/*",
cors({
origin: process.env.CORS_ORIGIN!,
origin: process.env.CORS_ORIGIN || "",
allowMethods: ["GET", "POST", "OPTIONS"],
allowHeaders: ["Content-Type", "Authorization"],
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",
dialect: "postgresql",
dbCredentials: {
url: process.env.POSTGRES_URL!,
url: process.env.POSTGRES_URL || "",
},
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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