Add todo example, remove yarn, change schema structure, update readme

This commit is contained in:
Aman Varshney
2025-03-24 00:04:53 +05:30
parent 5076bf4176
commit 4cc13bf382
42 changed files with 525 additions and 443 deletions

View File

@@ -0,0 +1,5 @@
---
"create-better-t-stack": major
---
stable release

View File

@@ -1,8 +1,6 @@
# Create Better-T-Stack CLI
> **Note:** This CLI is currently a work in progress (WIP).
An interactive CLI tool to quickly scaffold full-stack TypeScript applications using the Better-T-Stack framework.
An interactive CLI tool to quickly scaffold full-stack TypeScript applications with React, Hono, and tRPC. The Better-T-Stack provides a modern, type-safe development experience with the best tools from the TypeScript ecosystem.
## Quick Start
@@ -24,9 +22,12 @@ Follow the prompts to configure your project.
- **Database Options**: SQLite (via Turso), PostgreSQL, or no database
- **ORM Selection**: Choose between Drizzle ORM or Prisma
- **Authentication**: Optional auth setup with Better-Auth
- **Developer Experience**: Git initialization, various package manager support (npm, pnpm, yarn, bun)
- **Deployment**: Optional Docker configuration
- **CI/CD**: Optional GitHub Actions workflows
- **Progressive Web App**: Add PWA support with service workers and installable apps
- **Desktop Apps**: Build native desktop apps with Tauri integration
- **Code Quality**: Biome for linting and formatting
- **Git Hooks**: Husky with lint-staged for pre-commit checks
- **Examples**: Todo app with full CRUD functionality
- **Developer Experience**: Git initialization, various package manager support (npm, pnpm, bun)
## Usage
@@ -41,12 +42,17 @@ Options:
--postgres Use PostgreSQL database
--auth Include authentication
--no-auth Disable authentication
--docker Include Docker setup
--pwa Include Progressive Web App support
--tauri Include Tauri desktop app support
--biome Include Biome for linting and formatting
--husky Include Husky, lint-staged for Git hooks
--no-addons Skip all additional addons
--examples <examples> Include specified examples
--no-examples Skip all examples
--git Initialize a new git repo (default)
--no-git Skip git initialization
--npm Use npm as package manager
--pnpm Use pnpm as package manager
--yarn Use yarn as package manager
--bun Use bun as package manager
--drizzle Use Drizzle ORM
--prisma Use Prisma ORM
@@ -66,28 +72,11 @@ npx create-better-t-stack my-app -y
Create a project with specific options:
```bash
npx create-better-t-stack my-app --postgres --prisma --auth --docker
npx create-better-t-stack my-app --postgres --prisma --auth --pwa --biome
```
## Project Structure
The generated project follows a Turborepo monorepo structure:
```
my-app/
├── packages/
│ ├── client/ # Frontend application (React, TanStack Router)
│ └── server/ # Backend API (Hono, tRPC)
├── package.json # Root package.json with Turborepo configuration
└── README.md # Project documentation
```
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
## License
MIT
Created by [Nitish Singh](https://github.com/FgrReloaded) & [Aman Varshney](https://github.com/AmanVarshney01)
Created by [Aman Varshney](https://github.com/AmanVarshney01) & [Nitish Singh](https://github.com/FgrReloaded)

View File

@@ -22,10 +22,7 @@
"test": "vitest run",
"prepublishOnly": "npm run build"
},
"files": [
"dist",
"template"
],
"files": ["dist", "template"],
"dependencies": {
"@clack/prompts": "^0.10.0",
"commander": "^13.1.0",

View File

@@ -15,6 +15,7 @@ export const DEFAULT_CONFIG: ProjectConfig = {
git: true,
packageManager: "npm",
noInstall: false,
examples: ["todo"],
};
export const dependencyVersionMap = {

View File

@@ -1,16 +1,22 @@
import path from "node:path";
import { cancel, spinner } from "@clack/prompts";
import { $ } from "execa";
import fs from "fs-extra";
import pc from "picocolors";
import { PKG_ROOT } from "../constants";
import type { ProjectConfig, ProjectDatabase, ProjectOrm } from "../types";
import type { ProjectConfig } from "../types";
import { setupAddons } from "./addons-setup";
import { setupAuth } from "./auth-setup";
import { createReadme } from "./create-readme";
import { setupDatabase } from "./db-setup";
import { setupEnvironmentVariables } from "./env-setup";
import { setupExamples } from "./examples-setup";
import { displayPostInstallInstructions } from "./post-installation";
import { initializeGit, updatePackageConfigurations } from "./project-config";
import {
copyBaseTemplate,
fixGitignoreFiles,
setupAuthTemplate,
setupOrmTemplate,
} from "./template-manager";
export async function createProject(options: ProjectConfig): Promise<string> {
const s = spinner();
@@ -18,99 +24,26 @@ export async function createProject(options: ProjectConfig): Promise<string> {
try {
await fs.ensureDir(projectDir);
const templateDir = path.join(PKG_ROOT, "template/base");
if (!(await fs.pathExists(templateDir))) {
throw new Error(`Template directory not found: ${templateDir}`);
}
await fs.copy(templateDir, projectDir);
const gitignorePaths = [
path.join(projectDir, "_gitignore"),
path.join(projectDir, "packages/client/_gitignore"),
path.join(projectDir, "packages/server/_gitignore"),
];
await copyBaseTemplate(projectDir);
for (const gitignorePath of gitignorePaths) {
if (await fs.pathExists(gitignorePath)) {
const targetPath = path.join(path.dirname(gitignorePath), ".gitignore");
await fs.move(gitignorePath, targetPath);
}
}
await fixGitignoreFiles(projectDir);
if (options.auth) {
const authTemplateDir = path.join(PKG_ROOT, "template/with-auth");
if (await fs.pathExists(authTemplateDir)) {
await fs.copy(authTemplateDir, projectDir, { overwrite: true });
}
}
await setupAuthTemplate(projectDir, options.auth);
if (options.orm !== "none" && options.database !== "none") {
const ormTemplateDir = path.join(
PKG_ROOT,
getOrmTemplateDir(options.orm, options.database),
);
await setupOrmTemplate(
projectDir,
options.orm,
options.database,
options.auth,
);
if (await fs.pathExists(ormTemplateDir)) {
await fs.copy(ormTemplateDir, projectDir, { overwrite: true });
const serverSrcPath = path.join(projectDir, "packages/server/src");
const baseLibPath = path.join(serverSrcPath, "lib");
const withAuthLibPath = path.join(serverSrcPath, "with-auth-lib");
if (options.auth) {
await fs.remove(baseLibPath);
await fs.move(withAuthLibPath, baseLibPath);
if (options.orm === "prisma") {
const schemaPath = path.join(
projectDir,
"packages/server/prisma/schema.prisma",
);
const withAuthSchemaPath = path.join(
projectDir,
"packages/server/prisma/with-auth-schema.prisma",
);
if (await fs.pathExists(withAuthSchemaPath)) {
await fs.remove(schemaPath);
await fs.move(withAuthSchemaPath, schemaPath);
}
} else if (options.orm === "drizzle") {
const schemaPath = path.join(
projectDir,
"packages/server/src/db/schema.ts",
);
const withAuthSchemaPath = path.join(
projectDir,
"packages/server/src/db/with-auth-schema.ts",
);
if (await fs.pathExists(withAuthSchemaPath)) {
await fs.remove(schemaPath);
await fs.move(withAuthSchemaPath, schemaPath);
}
}
} else {
await fs.remove(withAuthLibPath);
if (options.orm === "prisma") {
const withAuthSchema = path.join(
projectDir,
"packages/server/prisma/with-auth-schema.prisma",
);
if (await fs.pathExists(withAuthSchema)) {
await fs.remove(withAuthSchema);
}
} else if (options.orm === "drizzle") {
const withAuthSchema = path.join(
projectDir,
"packages/server/src/db/with-auth-schema.ts",
);
if (await fs.pathExists(withAuthSchema)) {
await fs.remove(withAuthSchema);
}
}
}
}
}
await setupExamples(
projectDir,
options.examples,
options.orm,
options.auth,
);
await setupDatabase(
projectDir,
@@ -118,18 +51,19 @@ export async function createProject(options: ProjectConfig): Promise<string> {
options.orm,
options.turso ?? options.database === "sqlite",
);
await setupAuth(projectDir, options.auth);
await setupEnvironmentVariables(projectDir, options);
if (options.git) {
await $({ cwd: projectDir })`git init`;
}
await initializeGit(projectDir, options.git);
if (options.addons.length > 0) {
await setupAddons(projectDir, options.addons, options.packageManager);
}
await updatePackageConfigurations(projectDir, options);
await createReadme(projectDir, options);
displayPostInstallInstructions(
@@ -151,69 +85,3 @@ export async function createProject(options: ProjectConfig): Promise<string> {
throw error;
}
}
async function updatePackageConfigurations(
projectDir: string,
options: ProjectConfig,
) {
const rootPackageJsonPath = path.join(projectDir, "package.json");
if (await fs.pathExists(rootPackageJsonPath)) {
const packageJson = await fs.readJson(rootPackageJsonPath);
packageJson.name = options.projectName;
if (options.packageManager !== "bun") {
packageJson.packageManager =
options.packageManager === "npm"
? "npm@10.9.2"
: options.packageManager === "pnpm"
? "pnpm@10.6.4"
: options.packageManager === "yarn"
? "yarn@4.1.0"
: "bun@1.2.5";
}
await fs.writeJson(rootPackageJsonPath, packageJson, { spaces: 2 });
}
const serverPackageJsonPath = path.join(
projectDir,
"packages/server/package.json",
);
if (await fs.pathExists(serverPackageJsonPath)) {
const serverPackageJson = await fs.readJson(serverPackageJsonPath);
if (options.database !== "none") {
if (options.database === "sqlite") {
serverPackageJson.scripts["db:local"] = "turso dev --db-file local.db";
}
if (options.orm === "prisma") {
serverPackageJson.scripts["db:push"] = "prisma db push";
serverPackageJson.scripts["db:studio"] = "prisma studio";
} else if (options.orm === "drizzle") {
serverPackageJson.scripts["db:push"] = "drizzle-kit push";
serverPackageJson.scripts["db:studio"] = "drizzle-kit studio";
}
}
await fs.writeJson(serverPackageJsonPath, serverPackageJson, {
spaces: 2,
});
}
}
function getOrmTemplateDir(orm: ProjectOrm, database: ProjectDatabase): string {
if (orm === "drizzle") {
return database === "sqlite"
? "template/with-drizzle-sqlite"
: "template/with-drizzle-postgres";
}
if (orm === "prisma") {
return database === "sqlite"
? "template/with-prisma-sqlite"
: "template/with-prisma-postgres";
}
return "template/base";
}

View File

@@ -0,0 +1,130 @@
import path from "node:path";
import fs from "fs-extra";
import { PKG_ROOT } from "../constants";
import type { ProjectOrm } from "../types";
export async function setupExamples(
projectDir: string,
examples: string[],
orm: ProjectOrm,
auth: boolean,
): Promise<void> {
if (examples.includes("todo")) {
await setupTodoExample(projectDir, orm, auth);
} else {
await cleanupTodoFiles(projectDir, orm);
}
}
async function setupTodoExample(
projectDir: string,
orm: ProjectOrm,
auth: boolean,
): Promise<void> {
const todoExampleDir = path.join(PKG_ROOT, "template/examples/todo");
if (await fs.pathExists(todoExampleDir)) {
const todoRouteDir = path.join(
todoExampleDir,
"packages/client/src/routes",
);
const targetRouteDir = path.join(projectDir, "packages/client/src/routes");
await fs.copy(todoRouteDir, targetRouteDir, { overwrite: true });
if (orm !== "none") {
const todoRouterSourceFile = path.join(
todoExampleDir,
`packages/server/src/routers/with-${orm}-todo.ts`,
);
const todoRouterTargetFile = path.join(
projectDir,
"packages/server/src/routers/todo.ts",
);
if (await fs.pathExists(todoRouterSourceFile)) {
await fs.copy(todoRouterSourceFile, todoRouterTargetFile, {
overwrite: true,
});
}
}
await updateHeaderWithTodoLink(projectDir, auth);
}
}
async function updateHeaderWithTodoLink(
projectDir: string,
auth: boolean,
): Promise<void> {
const headerPath = path.join(
projectDir,
"packages/client/src/components/header.tsx",
);
if (await fs.pathExists(headerPath)) {
let headerContent = await fs.readFile(headerPath, "utf8");
if (auth) {
headerContent = headerContent.replace(
/const links = \[\s*{ to: "\/", label: "Home" },\s*{ to: "\/dashboard", label: "Dashboard" },/,
`const links = [\n { to: "/", label: "Home" },\n { to: "/dashboard", label: "Dashboard" },\n { to: "/todos", label: "Todos" },`,
);
} else {
headerContent = headerContent.replace(
/const links = \[\s*{ to: "\/", label: "Home" },/,
`const links = [\n { to: "/", label: "Home" },\n { to: "/todos", label: "Todos" },`,
);
}
await fs.writeFile(headerPath, headerContent);
}
}
async function cleanupTodoFiles(
projectDir: string,
orm: ProjectOrm,
): Promise<void> {
if (orm === "drizzle") {
const todoSchemaFile = path.join(
projectDir,
"packages/server/src/db/schema/todo.ts",
);
if (await fs.pathExists(todoSchemaFile)) {
await fs.remove(todoSchemaFile);
}
} else if (orm === "prisma") {
const todoPrismaFile = path.join(
projectDir,
"packages/server/prisma/schema/todo.prisma",
);
if (await fs.pathExists(todoPrismaFile)) {
await fs.remove(todoPrismaFile);
}
}
const todoRouterFile = path.join(
projectDir,
"packages/server/src/routers/todo.ts",
);
if (await fs.pathExists(todoRouterFile)) {
await fs.remove(todoRouterFile);
}
await updateRouterIndex(projectDir);
}
async function updateRouterIndex(projectDir: string): Promise<void> {
const routerFile = path.join(
projectDir,
"packages/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);
}
}

View File

@@ -1,8 +1,7 @@
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";
import type { PackageManager, ProjectAddons } from "../types";
export async function installDependencies({
projectDir,
@@ -26,7 +25,6 @@ export async function installDependencies({
})`${packageManager} install`;
break;
case "pnpm":
case "yarn":
case "bun":
await $({
cwd: projectDir,

View File

@@ -0,0 +1,76 @@
import path from "node:path";
import { $ } from "execa";
import fs from "fs-extra";
import type { ProjectConfig, ProjectDatabase, ProjectOrm } from "../types";
export async function updatePackageConfigurations(
projectDir: string,
options: ProjectConfig,
): Promise<void> {
await updateRootPackageJson(projectDir, options);
await updateServerPackageJson(projectDir, options);
}
async function updateRootPackageJson(
projectDir: string,
options: ProjectConfig,
): Promise<void> {
const rootPackageJsonPath = path.join(projectDir, "package.json");
if (await fs.pathExists(rootPackageJsonPath)) {
const packageJson = await fs.readJson(rootPackageJsonPath);
packageJson.name = options.projectName;
if (options.packageManager !== "bun") {
packageJson.packageManager =
options.packageManager === "npm"
? "npm@10.9.2"
: options.packageManager === "pnpm"
? "pnpm@10.6.4"
: "bun@1.2.5";
}
await fs.writeJson(rootPackageJsonPath, packageJson, { spaces: 2 });
}
}
async function updateServerPackageJson(
projectDir: string,
options: ProjectConfig,
): Promise<void> {
const serverPackageJsonPath = path.join(
projectDir,
"packages/server/package.json",
);
if (await fs.pathExists(serverPackageJsonPath)) {
const serverPackageJson = await fs.readJson(serverPackageJsonPath);
if (options.database !== "none") {
if (options.database === "sqlite") {
serverPackageJson.scripts["db:local"] = "turso dev --db-file local.db";
}
if (options.orm === "prisma") {
serverPackageJson.scripts["db:push"] =
"prisma db push --schema ./prisma/schema";
serverPackageJson.scripts["db:studio"] = "prisma studio";
} else if (options.orm === "drizzle") {
serverPackageJson.scripts["db:push"] = "drizzle-kit push";
serverPackageJson.scripts["db:studio"] = "drizzle-kit studio";
}
}
await fs.writeJson(serverPackageJsonPath, serverPackageJson, {
spaces: 2,
});
}
}
export async function initializeGit(
projectDir: string,
useGit: boolean,
): Promise<void> {
if (useGit) {
await $({ cwd: projectDir })`git init`;
}
}

View File

@@ -0,0 +1,83 @@
import path from "node:path";
import fs from "fs-extra";
import { PKG_ROOT } from "../constants";
import type { ProjectDatabase, ProjectOrm } from "../types";
export async function copyBaseTemplate(projectDir: string): Promise<void> {
const templateDir = path.join(PKG_ROOT, "template/base");
if (!(await fs.pathExists(templateDir))) {
throw new Error(`Template directory not found: ${templateDir}`);
}
await fs.copy(templateDir, projectDir);
}
export async function setupAuthTemplate(
projectDir: string,
auth: boolean,
): Promise<void> {
if (!auth) return;
const authTemplateDir = path.join(PKG_ROOT, "template/with-auth");
if (await fs.pathExists(authTemplateDir)) {
await fs.copy(authTemplateDir, projectDir, { overwrite: true });
}
}
export async function setupOrmTemplate(
projectDir: string,
orm: ProjectOrm,
database: ProjectDatabase,
auth: boolean,
): Promise<void> {
if (orm === "none" || database === "none") return;
const ormTemplateDir = path.join(PKG_ROOT, getOrmTemplateDir(orm, database));
if (await fs.pathExists(ormTemplateDir)) {
await fs.copy(ormTemplateDir, projectDir, { overwrite: true });
const serverSrcPath = path.join(projectDir, "packages/server/src");
const libPath = path.join(serverSrcPath, "lib");
const withAuthLibPath = path.join(serverSrcPath, "with-auth-lib");
if (auth) {
if (await fs.pathExists(withAuthLibPath)) {
await fs.remove(libPath);
await fs.move(withAuthLibPath, libPath);
}
} else {
await fs.remove(withAuthLibPath);
}
}
}
export async function fixGitignoreFiles(projectDir: string): Promise<void> {
const gitignorePaths = [
path.join(projectDir, "_gitignore"),
path.join(projectDir, "packages/client/_gitignore"),
path.join(projectDir, "packages/server/_gitignore"),
];
for (const gitignorePath of gitignorePaths) {
if (await fs.pathExists(gitignorePath)) {
const targetPath = path.join(path.dirname(gitignorePath), ".gitignore");
await fs.move(gitignorePath, targetPath);
}
}
}
function getOrmTemplateDir(orm: ProjectOrm, database: ProjectDatabase): string {
if (orm === "drizzle") {
return database === "sqlite"
? "template/with-drizzle-sqlite"
: "template/with-drizzle-postgres";
}
if (orm === "prisma") {
return database === "sqlite"
? "template/with-prisma-sqlite"
: "template/with-prisma-postgres";
}
return "template/base";
}

View File

@@ -5,7 +5,7 @@ import { DEFAULT_CONFIG } from "./constants";
import { createProject } from "./helpers/create-project";
import { installDependencies } from "./helpers/install-dependencies";
import { gatherConfig } from "./prompts/config-prompts";
import type { ProjectAddons, ProjectConfig } from "./types";
import type { ProjectAddons, ProjectConfig, ProjectExamples } from "./types";
import { displayConfig } from "./utils/display-config";
import { generateReproducibleCommand } from "./utils/generate-reproducible-command";
import { getLatestCLIVersion } from "./utils/get-latest-cli-version";
@@ -35,11 +35,12 @@ async function main() {
.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("--examples <examples>", "Include specified examples")
.option("--no-examples", "Skip all examples")
.option("--git", "Include git setup")
.option("--no-git", "Skip git initialization")
.option("--npm", "Use npm package manager")
.option("--pnpm", "Use pnpm package manager")
.option("--yarn", "Use yarn package manager")
.option("--bun", "Use bun package manager")
.option("--drizzle", "Use Drizzle ORM")
.option("--prisma", "Use Prisma ORM (coming soon)")
@@ -68,7 +69,6 @@ async function main() {
...("auth" in options && { auth: options.auth }),
...(options.npm && { packageManager: "npm" }),
...(options.pnpm && { packageManager: " pnpm" }),
...(options.yarn && { packageManager: "yarn" }),
...(options.bun && { packageManager: "bun" }),
...("git" in options && { git: options.git }),
...("install" in options && { noInstall: !options.install }),
@@ -88,6 +88,16 @@ async function main() {
...(options.husky ? ["husky"] : []),
] as ProjectAddons[]),
}),
...((options.examples || options.examples === false) && {
examples:
options.examples === false
? []
: typeof options.examples === "string"
? (options.examples
.split(",")
.filter((e) => e === "todo") as ProjectExamples[])
: [],
}),
};
if (!options.yes && Object.keys(flagConfig).length > 0) {
@@ -123,6 +133,9 @@ async function main() {
addons: flagConfig.addons?.length
? flagConfig.addons
: DEFAULT_CONFIG.addons,
examples: flagConfig.examples?.length
? flagConfig.examples
: DEFAULT_CONFIG.examples,
turso:
"turso" in options
? options.turso

View File

@@ -5,11 +5,13 @@ import type {
ProjectAddons,
ProjectConfig,
ProjectDatabase,
ProjectExamples,
ProjectOrm,
} from "../types";
import { getAddonsChoice } from "./addons";
import { getAuthChoice } from "./auth";
import { getDatabaseChoice } from "./database";
import { getExamplesChoice } from "./examples";
import { getGitChoice } from "./git";
import { getNoInstallChoice } from "./install";
import { getORMChoice } from "./orm";
@@ -23,6 +25,7 @@ interface PromptGroupResults {
orm: ProjectOrm;
auth: boolean;
addons: ProjectAddons[];
examples: ProjectExamples[];
git: boolean;
packageManager: PackageManager;
noInstall: boolean;
@@ -47,6 +50,7 @@ export async function gatherConfig(
? getTursoSetupChoice(flags.turso)
: Promise.resolve(false),
addons: () => getAddonsChoice(flags.addons),
examples: () => getExamplesChoice(flags.examples),
git: () => getGitChoice(flags.git),
packageManager: () => getPackageManagerChoice(flags.packageManager),
noInstall: () => getNoInstallChoice(flags.noInstall),
@@ -65,6 +69,7 @@ export async function gatherConfig(
orm: result.orm,
auth: result.auth,
addons: result.addons,
examples: result.examples,
git: result.git,
packageManager: result.packageManager,
noInstall: result.noInstall,

View File

@@ -0,0 +1,30 @@
import { cancel, isCancel, multiselect } from "@clack/prompts";
import pc from "picocolors";
import { DEFAULT_CONFIG } from "../constants";
import type { ProjectExamples } from "../types";
export async function getExamplesChoice(
examples?: ProjectExamples[],
): Promise<ProjectExamples[]> {
if (examples !== undefined) return examples;
const response = await multiselect<ProjectExamples>({
message: "Which examples would you like to include?",
options: [
{
value: "todo",
label: "Todo App",
hint: "A simple CRUD example app",
},
],
required: false,
initialValues: DEFAULT_CONFIG.examples,
});
if (isCancel(response)) {
cancel(pc.red("Operation cancelled"));
process.exit(0);
}
return response;
}

View File

@@ -24,11 +24,6 @@ export async function getPackageManagerChoice(
label: "pnpm",
hint: "Fast, disk space efficient package manager",
},
{
value: "yarn",
label: "yarn",
hint: "Fast, reliable, and secure dependency management",
},
],
initialValue: detectedPackageManager,
});

View File

@@ -1,7 +1,8 @@
export type ProjectDatabase = "sqlite" | "postgres" | "none";
export type ProjectOrm = "drizzle" | "prisma" | "none";
export type PackageManager = "npm" | "pnpm" | "yarn" | "bun";
export type PackageManager = "npm" | "pnpm" | "bun";
export type ProjectAddons = "pwa" | "tauri" | "biome" | "husky";
export type ProjectExamples = "todo";
export interface ProjectConfig {
projectName: string;
@@ -9,6 +10,7 @@ export interface ProjectConfig {
orm: ProjectOrm;
auth: boolean;
addons: ProjectAddons[];
examples: ProjectExamples[];
git: boolean;
packageManager: PackageManager;
noInstall?: boolean;

View File

@@ -49,6 +49,20 @@ export function generateReproducibleCommand(config: ProjectConfig): string {
flags.push("--no-addons");
}
if (config.examples && config.examples.length > 0) {
flags.push(`--examples ${config.examples.join(",")}`);
} else {
flags.push("--no-examples");
}
if (config.database === "sqlite") {
if (config.turso) {
flags.push("--turso");
} else {
flags.push("--no-turso");
}
}
const baseCommand = "npx create-better-t-stack";
const projectName = config.projectName ? ` ${config.projectName}` : "";
const flagString = flags.length > 0 ? ` ${flags.join(" ")}` : "";

View File

@@ -1,11 +1,8 @@
export type PackageManager = "npm" | "pnpm" | "yarn" | "bun";
import type { PackageManager } from "../types";
export const getUserPkgManager: () => PackageManager = () => {
const userAgent = process.env.npm_config_user_agent;
if (userAgent?.startsWith("yarn")) {
return "yarn";
}
if (userAgent?.startsWith("pnpm")) {
return "pnpm";
}

View File

@@ -2,30 +2,26 @@ import { Link } from "@tanstack/react-router";
import { ModeToggle } from "./mode-toggle";
export default function Header() {
const links = [
{ to: "/", label: "Home" },
];
return (
<div>
<div className="flex flex-row items-center justify-between px-2 py-1">
<div className="flex gap-4 text-lg">
<Link
to="/"
activeProps={{
className: "font-bold",
}}
activeOptions={{ exact: true }}
>
Home
</Link>
<Link
to="/todos"
activeProps={{
className: "font-bold",
}}
activeOptions={{ exact: true }}
>
Todos
</Link>
</div>
<div className="flex flex-row items-center gap-2">
<nav className="flex gap-4 text-lg">
{links.map(({ to, label }) => (
<Link
key={to}
to={to}
activeProps={{ className: "font-bold" }}
activeOptions={{ exact: true }}
>
{label}
</Link>
))}
</nav>
<div className="flex items-center gap-2">
<ModeToggle />
</div>
</div>

View File

@@ -29,7 +29,7 @@ app.use(
}),
);
app.get("/healthCheck", (c) => {
app.get("/", (c) => {
return c.text("OK");
});

View File

@@ -1,6 +1,6 @@
import { z } from "zod";
import { router, publicProcedure } from "../lib/trpc";
import { todo } from "../db/schema";
import { todo } from "../db/schema/todo";
import { eq } from "drizzle-orm";
import { db } from "../db";

View File

@@ -3,39 +3,27 @@ import { ModeToggle } from "./mode-toggle";
import UserMenu from "./user-menu";
export default function Header() {
const links = [
{ to: "/", label: "Home" },
{ to: "/dashboard", label: "Dashboard" },
];
return (
<div>
<div className="flex flex-row items-center justify-between px-2 py-1">
<div className="flex gap-4 text-lg">
<Link
to="/"
activeProps={{
className: "font-bold",
}}
activeOptions={{ exact: true }}
>
Home
</Link>
<Link
to="/todos"
activeProps={{
className: "font-bold",
}}
activeOptions={{ exact: true }}
>
Todos
</Link>
<Link
to="/dashboard"
activeProps={{
className: "font-bold",
}}
activeOptions={{ exact: true }}
>
Dashboard
</Link>
</div>
<div className="flex flex-row items-center gap-2">
<nav className="flex gap-4 text-lg">
{links.map(({ to, label }) => (
<Link
key={to}
to={to}
activeProps={{ className: "font-bold" }}
activeOptions={{ exact: true }}
>
{label}
</Link>
))}
</nav>
<div className="flex items-center gap-2">
<ModeToggle />
<UserMenu />
</div>

View File

@@ -34,7 +34,7 @@ app.use(
}),
);
app.get("/healthCheck", (c) => {
app.get("/", (c) => {
return c.text("OK");
});

View File

@@ -1,8 +1,8 @@
import { defineConfig } from "drizzle-kit";
export default defineConfig({
schema: "./src/db/schema.ts",
out: "./migrations",
schema: "./src/db/schema",
out: "./src/db/migrations",
dialect: "postgresql",
dbCredentials: {
url: process.env.POSTGRES_URL || "",

View File

@@ -1,10 +1,4 @@
import { pgTable, text, integer, timestamp, boolean } from "drizzle-orm/pg-core";
export const todo = pgTable("todo", {
id: serial("id").primaryKey(),
text: text("text").notNull(),
completed: boolean("completed").default(false).notNull()
});
import { pgTable, text, timestamp, boolean, serial } from "drizzle-orm/pg-core";
export const user = pgTable("user", {
id: text("id").primaryKey(),

View File

@@ -1,4 +1,4 @@
import { pgTable, text, integer, timestamp, boolean } from "drizzle-orm/pg-core";
import { pgTable, text, boolean, serial } from "drizzle-orm/pg-core";
export const todo = pgTable("todo", {
id: serial("id").primaryKey(),

View File

@@ -1,7 +1,7 @@
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { db } from "../db";
import * as schema from "../db/schema";
import * as schema from "../db/schema/auth";
export const auth = betterAuth({
database: drizzleAdapter(db, {

View File

@@ -1,8 +1,8 @@
import { defineConfig } from "drizzle-kit";
export default defineConfig({
schema: "./src/db/schema.ts",
out: "./migrations",
schema: "./src/db/schema",
out: "./src/db/migrations",
dialect: "turso",
dbCredentials: {
url: process.env.TURSO_CONNECTION_URL || "",

View File

@@ -1,7 +1,7 @@
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { db } from "../db";
import * as schema from "../db/schema";
import * as schema from "../db/schema/auth";
export const auth = betterAuth({
database: drizzleAdapter(db, {

View File

@@ -1,20 +1,3 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = "file:./local.db"
}
model Todo {
id Int @id @default(autoincrement())
text String
completed Boolean @default(false)
@@map("todo")
}
model User {
id String @id @map("_id")
name String

View File

@@ -0,0 +1,9 @@
generator client {
provider = "prisma-client-js"
previewFeatures = ["prismaSchemaFolder"]
}
datasource db {
provider = "postgres"
url = env("DATABASE_URL")
}

View File

@@ -1,12 +1,3 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = "file:./local.db"
}
model Todo {
id Int @id @default(autoincrement())
text String

View File

@@ -1,20 +1,3 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgres"
url = env("DATABASE_URL")
}
model Todo {
id Int @id @default(autoincrement())
text String
completed Boolean @default(false)
@@map("todo")
}
model User {
id String @id @map("_id")
name String

View File

@@ -0,0 +1,8 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = "file:../local.db"
}

View File

@@ -1,12 +1,3 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgres"
url = env("DATABASE_URL")
}
model Todo {
id Int @id @default(autoincrement())
text String

View File

@@ -1,55 +0,0 @@
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import prisma from "../../prisma";
import { publicProcedure, router } from "../lib/trpc";
export const todoRouter = router({
getAll: publicProcedure.query(async () => {
return await prisma.todo.findMany({
orderBy: {
id: "asc"
}
});
}),
create: publicProcedure
.input(z.object({ text: z.string().min(1) }))
.mutation(async ({ input }) => {
return await prisma.todo.create({
data: {
text: input.text,
},
});
}),
toggle: publicProcedure
.input(z.object({ id: z.number(), completed: z.boolean() }))
.mutation(async ({ input }) => {
try {
return await prisma.todo.update({
where: { id: input.id },
data: { completed: input.completed },
});
} catch (error) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Todo not found",
});
}
}),
delete: publicProcedure
.input(z.object({ id: z.number() }))
.mutation(async ({ input }) => {
try {
return await prisma.todo.delete({
where: { id: input.id },
});
} catch (error) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Todo not found",
});
}
}),
});

View File

@@ -1,26 +1,47 @@
# web
# Better-T-Stack Website
This is a Next.js application generated with
[Create Fumadocs](https://github.com/fuma-nama/fumadocs).
This is the official documentation website for Better-T-Stack, built with Next.js and Fumadocs.
Run development server:
## Getting Started
To run the development server:
```bash
# Install dependencies
npm install
# or
pnpm install
# or
bun install
# Start development server
npm run dev
# or
pnpm dev
# or
yarn dev
bun dev
```
Open http://localhost:3000 with your browser to see the result.
Open [http://localhost:3000](http://localhost:3000) with your browser to see the site.
## Project Structure
- `/src/app` - Next.js application routes
- `/src/content` - Documentation content in MDX format
- `/public` - Static assets
## Contributing to Documentation
To add or modify documentation:
1. Edit the appropriate MDX files in the `src/content` directory
2. Run the development server to preview your changes
3. Submit a pull request with your updates
## Learn More
To learn more about Next.js and Fumadocs, take a look at the following
resources:
To learn more about the technologies used in this website:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js
features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
- [Fumadocs](https://fumadocs.vercel.app) - learn about Fumadocs
- [Next.js Documentation](https://nextjs.org/docs) - Next.js features and API
- [Fumadocs](https://fumadocs.vercel.app) - The documentation framework used
- [Better-T-Stack](https://better-t-stack.pages.dev) - Main project site

View File

@@ -3,9 +3,7 @@ import { useEffect, useRef, useState } from "react";
const CodeContainer = () => {
const [isOpen, setIsOpen] = useState(false);
const [selectedPM, setSelectedPM] = useState<"npm" | "yarn" | "pnpm" | "bun">(
"npm",
);
const [selectedPM, setSelectedPM] = useState<"npm" | "pnpm" | "bun">("npm");
const [copied, setCopied] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
const [typingComplete, setTypingComplete] = useState(false);
@@ -24,12 +22,11 @@ const CodeContainer = () => {
const commands = {
npm: "npx create-better-t-stack@latest",
yarn: "yarn dlx create-better-t-stack",
pnpm: "pnpm dlx create-better-t-stack",
bun: "bunx create-better-t-stack",
};
const copyToClipboard = async (pm: "npm" | "yarn" | "pnpm" | "bun") => {
const copyToClipboard = async (pm: "npm" | "pnpm" | "bun") => {
await navigator.clipboard.writeText(commands[pm]);
setSelectedPM(pm);
setCopied(true);
@@ -93,25 +90,23 @@ const CodeContainer = () => {
{isOpen && (
<div className="absolute right-0 mt-2 w-36 bg-black border border-blue-500/30 rounded-md shadow-lg z-50">
<ul>
{(
Object.keys(commands) as Array<
"npm" | "yarn" | "pnpm" | "bun"
>
).map((pm) => (
<li key={pm}>
<button
type="button"
className={`block w-full text-left px-4 py-2 text-sm ${
selectedPM === pm
? "bg-blue-900/30 text-blue-400"
: "text-gray-300 hover:bg-blue-900/20"
}`}
onClick={() => copyToClipboard(pm)}
>
{pm}
</button>
</li>
))}
{(Object.keys(commands) as Array<"npm" | "pnpm" | "bun">).map(
(pm) => (
<li key={pm}>
<button
type="button"
className={`block w-full text-left px-4 py-2 text-sm ${
selectedPM === pm
? "bg-blue-900/30 text-blue-400"
: "text-gray-300 hover:bg-blue-900/20"
}`}
onClick={() => copyToClipboard(pm)}
>
{pm}
</button>
</li>
),
)}
</ul>
</div>
)}

View File

@@ -7,30 +7,6 @@ const PackageIcon = ({ pm, className }: { pm: string; className?: string }) => {
<path d="M1.763 0C.786 0 0 .786 0 1.763v20.474C0 23.214.786 24 1.763 24h20.474c.977 0 1.763-.786 1.763-1.763V1.763C24 .786 23.214 0 22.237 0zM5.13 5.323l13.837.019-.009 13.836h-3.464l.01-10.382h-3.456L12.04 19.17H5.113z" />
</svg>
);
case "yarn":
return (
<svg
className={className}
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
fillRule="evenodd"
clipRule="evenodd"
strokeLinejoin="round"
strokeMiterlimit="2"
>
<title>yarn</title>
<path
d="M256 0c141.344 0 256 114.656 256 256S397.344 512 256 512 0 397.344 0 256 114.656 0 256 0z"
fill="#2c8ebb"
fillRule="nonzero"
/>
<path
d="M430.16 333.59c-1.78-14.035-13.641-23.721-28.863-23.524-22.733.297-41.81 12.06-54.461 19.868-4.943 3.064-9.193 5.337-12.85 7.017.79-11.465.099-26.49-5.832-42.996-7.215-19.768-16.901-31.926-23.82-38.943 8.006-11.664 18.977-28.665 24.117-54.956 4.448-22.437 3.064-57.329-7.117-76.9-2.075-3.953-5.535-6.82-9.884-8.005-1.779-.495-5.14-1.483-11.762.395-9.983-20.658-13.442-22.832-16.111-24.612-5.535-3.558-12.059-4.349-18.187-2.075-8.204 2.965-15.222 10.872-21.844 24.908-.988 2.075-1.878 4.052-2.669 6.03-12.553.889-32.321 5.435-49.025 23.523-2.076 2.274-6.128 3.954-10.379 5.536h.1c-8.699 3.064-12.653 10.18-17.496 23.03-6.721 17.989.198 35.682 7.018 47.147-9.291 8.303-21.646 21.548-28.17 37.066-8.105 19.175-8.994 37.955-8.698 48.136-6.919 7.314-17.594 21.053-18.78 36.472-1.581 21.548 6.227 36.176 9.687 41.514.988 1.581 2.075 2.866 3.261 4.151-.395 2.669-.494 5.535.1 8.5 1.284 6.92 5.633 12.553 12.256 16.112 13.047 6.919 31.234 9.884 45.27 2.866 5.04 5.338 14.232 10.477 30.937 10.477h.988c4.25 0 58.218-2.866 73.934-6.72 7.017-1.681 11.86-4.646 15.023-7.315 10.082-3.163 37.956-12.652 64.248-29.653 18.582-12.058 25.007-14.628 38.844-17.989 13.443-3.262 21.844-15.518 20.164-29.06zm-23.525 14.53c-15.815 3.756-23.821 7.216-43.392 19.966-30.542 19.769-63.95 28.961-63.95 28.961s-2.768 4.151-10.774 6.03c-13.838 3.36-65.927 6.226-70.672 6.325-12.75.1-20.559-3.261-22.733-8.5-6.623-15.815 9.488-22.734 9.488-22.734s-3.558-2.174-5.634-4.151c-1.878-1.878-3.854-5.634-4.448-4.25-2.47 6.03-3.756 20.757-10.378 27.379-9.093 9.192-26.292 6.128-36.473.79-11.169-5.93.791-19.866.791-19.866s-6.03 3.558-10.872-3.756c-4.35-6.722-8.402-18.187-7.315-32.322 1.186-16.11 19.176-31.728 19.176-31.728s-3.163-23.82 7.215-48.235c9.39-22.239 34.694-40.13 34.694-40.13s-21.251-23.524-13.344-44.676c5.14-13.838 7.215-13.739 8.896-14.332 5.93-2.273 11.663-4.744 15.913-9.39 21.251-22.931 48.334-18.582 48.334-18.582s12.85-39.043 24.71-31.432c3.657 2.372 16.803 31.63 16.803 31.63s14.036-8.204 15.617-5.14c8.5 16.506 9.49 48.037 5.733 67.212-6.326 31.63-22.14 48.63-28.466 59.305-1.483 2.471 17 10.28 28.664 42.601 10.774 29.554 1.186 54.363 2.866 57.13.297.495.396.692.396.692s12.355.989 37.164-14.332c13.245-8.204 28.96-17.396 46.851-17.593 17.297-.297 18.187 19.966 5.14 23.128z"
fill="#fff"
fillRule="nonzero"
/>
</svg>
);
case "pnpm":
return (
<svg

View File

@@ -35,7 +35,6 @@ const techOptions: Record<string, TechOption[]> = {
packageManager: [
{ id: "npm", label: "NPM", category: "packageManager" },
{ id: "pnpm", label: "PNPM", category: "packageManager" },
{ id: "yarn", label: "Yarn", category: "packageManager" },
{ id: "bun", label: "Bun", category: "packageManager" },
],
addons: [