diff --git a/.changeset/true-singers-scream.md b/.changeset/true-singers-scream.md new file mode 100644 index 0000000..8d17120 --- /dev/null +++ b/.changeset/true-singers-scream.md @@ -0,0 +1,5 @@ +--- +"create-better-t-stack": major +--- + +stable release diff --git a/apps/cli/README.md b/apps/cli/README.md index 4c3f6e2..b13fc29 100644 --- a/apps/cli/README.md +++ b/apps/cli/README.md @@ -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 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) diff --git a/apps/cli/package.json b/apps/cli/package.json index aa2026a..377f294 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -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", diff --git a/apps/cli/src/constants.ts b/apps/cli/src/constants.ts index fb24d8a..94b16da 100644 --- a/apps/cli/src/constants.ts +++ b/apps/cli/src/constants.ts @@ -15,6 +15,7 @@ export const DEFAULT_CONFIG: ProjectConfig = { git: true, packageManager: "npm", noInstall: false, + examples: ["todo"], }; export const dependencyVersionMap = { diff --git a/apps/cli/src/helpers/create-project.ts b/apps/cli/src/helpers/create-project.ts index 4658ddb..e0f8d3e 100644 --- a/apps/cli/src/helpers/create-project.ts +++ b/apps/cli/src/helpers/create-project.ts @@ -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 { const s = spinner(); @@ -18,99 +24,26 @@ export async function createProject(options: ProjectConfig): Promise { 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 { 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 { 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"; -} diff --git a/apps/cli/src/helpers/examples-setup.ts b/apps/cli/src/helpers/examples-setup.ts new file mode 100644 index 0000000..a44a4b3 --- /dev/null +++ b/apps/cli/src/helpers/examples-setup.ts @@ -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 { + if (examples.includes("todo")) { + await setupTodoExample(projectDir, orm, auth); + } else { + await cleanupTodoFiles(projectDir, orm); + } +} + +async function setupTodoExample( + projectDir: string, + orm: ProjectOrm, + auth: boolean, +): Promise { + 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 { + 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 { + 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 { + 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); + } +} diff --git a/apps/cli/src/helpers/install-dependencies.ts b/apps/cli/src/helpers/install-dependencies.ts index c2f8d73..ba07516 100644 --- a/apps/cli/src/helpers/install-dependencies.ts +++ b/apps/cli/src/helpers/install-dependencies.ts @@ -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, diff --git a/apps/cli/src/helpers/project-config.ts b/apps/cli/src/helpers/project-config.ts new file mode 100644 index 0000000..c1592a7 --- /dev/null +++ b/apps/cli/src/helpers/project-config.ts @@ -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 { + await updateRootPackageJson(projectDir, options); + await updateServerPackageJson(projectDir, options); +} + +async function updateRootPackageJson( + projectDir: string, + options: ProjectConfig, +): Promise { + 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 { + 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 { + if (useGit) { + await $({ cwd: projectDir })`git init`; + } +} diff --git a/apps/cli/src/helpers/template-manager.ts b/apps/cli/src/helpers/template-manager.ts new file mode 100644 index 0000000..8527feb --- /dev/null +++ b/apps/cli/src/helpers/template-manager.ts @@ -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 { + 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 { + 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 { + 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 { + 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"; +} diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts index 0e6c6bd..d3266b0 100644 --- a/apps/cli/src/index.ts +++ b/apps/cli/src/index.ts @@ -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 ", "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 diff --git a/apps/cli/src/prompts/config-prompts.ts b/apps/cli/src/prompts/config-prompts.ts index d3b3ed4..2c8aa46 100644 --- a/apps/cli/src/prompts/config-prompts.ts +++ b/apps/cli/src/prompts/config-prompts.ts @@ -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, diff --git a/apps/cli/src/prompts/examples.ts b/apps/cli/src/prompts/examples.ts new file mode 100644 index 0000000..a274b9b --- /dev/null +++ b/apps/cli/src/prompts/examples.ts @@ -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 { + if (examples !== undefined) return examples; + + const response = await multiselect({ + 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; +} diff --git a/apps/cli/src/prompts/package-manager.ts b/apps/cli/src/prompts/package-manager.ts index 8933cab..76a0ef7 100644 --- a/apps/cli/src/prompts/package-manager.ts +++ b/apps/cli/src/prompts/package-manager.ts @@ -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, }); diff --git a/apps/cli/src/types.ts b/apps/cli/src/types.ts index a844f77..a79d1d5 100644 --- a/apps/cli/src/types.ts +++ b/apps/cli/src/types.ts @@ -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; diff --git a/apps/cli/src/utils/generate-reproducible-command.ts b/apps/cli/src/utils/generate-reproducible-command.ts index 7a9a18d..f761ccb 100644 --- a/apps/cli/src/utils/generate-reproducible-command.ts +++ b/apps/cli/src/utils/generate-reproducible-command.ts @@ -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(" ")}` : ""; diff --git a/apps/cli/src/utils/get-package-manager.ts b/apps/cli/src/utils/get-package-manager.ts index add4eb0..26f739a 100644 --- a/apps/cli/src/utils/get-package-manager.ts +++ b/apps/cli/src/utils/get-package-manager.ts @@ -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"; } diff --git a/apps/cli/template/base/packages/client/src/components/header.tsx b/apps/cli/template/base/packages/client/src/components/header.tsx index 894fbbd..af279f7 100644 --- a/apps/cli/template/base/packages/client/src/components/header.tsx +++ b/apps/cli/template/base/packages/client/src/components/header.tsx @@ -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 (
-
- - Home - - - Todos - -
-
+ +
diff --git a/apps/cli/template/base/packages/server/src/index.ts b/apps/cli/template/base/packages/server/src/index.ts index df4f3cd..d7a44e8 100644 --- a/apps/cli/template/base/packages/server/src/index.ts +++ b/apps/cli/template/base/packages/server/src/index.ts @@ -29,7 +29,7 @@ app.use( }), ); -app.get("/healthCheck", (c) => { +app.get("/", (c) => { return c.text("OK"); }); diff --git a/apps/cli/template/base/packages/client/src/routes/todos.tsx b/apps/cli/template/examples/todo/packages/client/src/routes/todos.tsx similarity index 100% rename from apps/cli/template/base/packages/client/src/routes/todos.tsx rename to apps/cli/template/examples/todo/packages/client/src/routes/todos.tsx diff --git a/apps/cli/template/with-drizzle-sqlite/packages/server/src/routers/todo.ts b/apps/cli/template/examples/todo/packages/server/src/routers/with-drizzle-todo.ts similarity index 96% rename from apps/cli/template/with-drizzle-sqlite/packages/server/src/routers/todo.ts rename to apps/cli/template/examples/todo/packages/server/src/routers/with-drizzle-todo.ts index e2b1896..a692c60 100644 --- a/apps/cli/template/with-drizzle-sqlite/packages/server/src/routers/todo.ts +++ b/apps/cli/template/examples/todo/packages/server/src/routers/with-drizzle-todo.ts @@ -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"; diff --git a/apps/cli/template/with-prisma-postgres/packages/server/src/routers/todo.ts b/apps/cli/template/examples/todo/packages/server/src/routers/with-prisma-todo.ts similarity index 100% rename from apps/cli/template/with-prisma-postgres/packages/server/src/routers/todo.ts rename to apps/cli/template/examples/todo/packages/server/src/routers/with-prisma-todo.ts diff --git a/apps/cli/template/with-auth/packages/client/src/components/header.tsx b/apps/cli/template/with-auth/packages/client/src/components/header.tsx index 30b36b9..b3c4ef8 100644 --- a/apps/cli/template/with-auth/packages/client/src/components/header.tsx +++ b/apps/cli/template/with-auth/packages/client/src/components/header.tsx @@ -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 (
-
- - Home - - - Todos - - - Dashboard - -
-
+ +
diff --git a/apps/cli/template/with-auth/packages/server/src/index.ts b/apps/cli/template/with-auth/packages/server/src/index.ts index 335c506..90bb7c1 100644 --- a/apps/cli/template/with-auth/packages/server/src/index.ts +++ b/apps/cli/template/with-auth/packages/server/src/index.ts @@ -34,7 +34,7 @@ app.use( }), ); -app.get("/healthCheck", (c) => { +app.get("/", (c) => { return c.text("OK"); }); diff --git a/apps/cli/template/with-drizzle-postgres/packages/server/drizzle.config.ts b/apps/cli/template/with-drizzle-postgres/packages/server/drizzle.config.ts index 78168de..f16f1ce 100644 --- a/apps/cli/template/with-drizzle-postgres/packages/server/drizzle.config.ts +++ b/apps/cli/template/with-drizzle-postgres/packages/server/drizzle.config.ts @@ -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 || "", diff --git a/apps/cli/template/with-drizzle-postgres/packages/server/src/db/with-auth-schema.ts b/apps/cli/template/with-drizzle-postgres/packages/server/src/db/schema/auth.ts similarity index 87% rename from apps/cli/template/with-drizzle-postgres/packages/server/src/db/with-auth-schema.ts rename to apps/cli/template/with-drizzle-postgres/packages/server/src/db/schema/auth.ts index e64d0c4..400bbb8 100644 --- a/apps/cli/template/with-drizzle-postgres/packages/server/src/db/with-auth-schema.ts +++ b/apps/cli/template/with-drizzle-postgres/packages/server/src/db/schema/auth.ts @@ -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(), diff --git a/apps/cli/template/with-drizzle-postgres/packages/server/src/db/schema.ts b/apps/cli/template/with-drizzle-postgres/packages/server/src/db/schema/todo.ts similarity index 67% rename from apps/cli/template/with-drizzle-postgres/packages/server/src/db/schema.ts rename to apps/cli/template/with-drizzle-postgres/packages/server/src/db/schema/todo.ts index 7725d84..7246649 100644 --- a/apps/cli/template/with-drizzle-postgres/packages/server/src/db/schema.ts +++ b/apps/cli/template/with-drizzle-postgres/packages/server/src/db/schema/todo.ts @@ -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(), diff --git a/apps/cli/template/with-drizzle-postgres/packages/server/src/with-auth-lib/auth.ts b/apps/cli/template/with-drizzle-postgres/packages/server/src/with-auth-lib/auth.ts index 3ef3946..ed45465 100644 --- a/apps/cli/template/with-drizzle-postgres/packages/server/src/with-auth-lib/auth.ts +++ b/apps/cli/template/with-drizzle-postgres/packages/server/src/with-auth-lib/auth.ts @@ -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, { diff --git a/apps/cli/template/with-drizzle-sqlite/packages/server/drizzle.config.ts b/apps/cli/template/with-drizzle-sqlite/packages/server/drizzle.config.ts index fb4ba63..a36b162 100644 --- a/apps/cli/template/with-drizzle-sqlite/packages/server/drizzle.config.ts +++ b/apps/cli/template/with-drizzle-sqlite/packages/server/drizzle.config.ts @@ -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 || "", diff --git a/apps/cli/template/with-drizzle-sqlite/packages/server/src/db/with-auth-schema.ts b/apps/cli/template/with-drizzle-sqlite/packages/server/src/db/schema/auth.ts similarity index 100% rename from apps/cli/template/with-drizzle-sqlite/packages/server/src/db/with-auth-schema.ts rename to apps/cli/template/with-drizzle-sqlite/packages/server/src/db/schema/auth.ts diff --git a/apps/cli/template/with-drizzle-sqlite/packages/server/src/db/schema.ts b/apps/cli/template/with-drizzle-sqlite/packages/server/src/db/schema/todo.ts similarity index 100% rename from apps/cli/template/with-drizzle-sqlite/packages/server/src/db/schema.ts rename to apps/cli/template/with-drizzle-sqlite/packages/server/src/db/schema/todo.ts diff --git a/apps/cli/template/with-drizzle-sqlite/packages/server/src/with-auth-lib/auth.ts b/apps/cli/template/with-drizzle-sqlite/packages/server/src/with-auth-lib/auth.ts index 49511e5..8b3bafc 100644 --- a/apps/cli/template/with-drizzle-sqlite/packages/server/src/with-auth-lib/auth.ts +++ b/apps/cli/template/with-drizzle-sqlite/packages/server/src/with-auth-lib/auth.ts @@ -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, { diff --git a/apps/cli/template/with-prisma-sqlite/packages/server/prisma/with-auth-schema.prisma b/apps/cli/template/with-prisma-postgres/packages/server/prisma/schema/auth.prisma similarity index 83% rename from apps/cli/template/with-prisma-sqlite/packages/server/prisma/with-auth-schema.prisma rename to apps/cli/template/with-prisma-postgres/packages/server/prisma/schema/auth.prisma index 57a7448..8e5fe38 100644 --- a/apps/cli/template/with-prisma-sqlite/packages/server/prisma/with-auth-schema.prisma +++ b/apps/cli/template/with-prisma-postgres/packages/server/prisma/schema/auth.prisma @@ -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 diff --git a/apps/cli/template/with-prisma-postgres/packages/server/prisma/schema/schema.prisma b/apps/cli/template/with-prisma-postgres/packages/server/prisma/schema/schema.prisma new file mode 100644 index 0000000..5e2a267 --- /dev/null +++ b/apps/cli/template/with-prisma-postgres/packages/server/prisma/schema/schema.prisma @@ -0,0 +1,9 @@ +generator client { + provider = "prisma-client-js" + previewFeatures = ["prismaSchemaFolder"] +} + +datasource db { + provider = "postgres" + url = env("DATABASE_URL") +} diff --git a/apps/cli/template/with-prisma-sqlite/packages/server/prisma/schema.prisma b/apps/cli/template/with-prisma-postgres/packages/server/prisma/schema/todo.prisma similarity index 52% rename from apps/cli/template/with-prisma-sqlite/packages/server/prisma/schema.prisma rename to apps/cli/template/with-prisma-postgres/packages/server/prisma/schema/todo.prisma index 36c95c7..4f23d6f 100644 --- a/apps/cli/template/with-prisma-sqlite/packages/server/prisma/schema.prisma +++ b/apps/cli/template/with-prisma-postgres/packages/server/prisma/schema/todo.prisma @@ -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 diff --git a/apps/cli/template/with-prisma-postgres/packages/server/prisma/with-auth-schema.prisma b/apps/cli/template/with-prisma-sqlite/packages/server/prisma/schema/auth.prisma similarity index 83% rename from apps/cli/template/with-prisma-postgres/packages/server/prisma/with-auth-schema.prisma rename to apps/cli/template/with-prisma-sqlite/packages/server/prisma/schema/auth.prisma index 6612705..8e5fe38 100644 --- a/apps/cli/template/with-prisma-postgres/packages/server/prisma/with-auth-schema.prisma +++ b/apps/cli/template/with-prisma-sqlite/packages/server/prisma/schema/auth.prisma @@ -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 diff --git a/apps/cli/template/with-prisma-sqlite/packages/server/prisma/schema/schema.prisma b/apps/cli/template/with-prisma-sqlite/packages/server/prisma/schema/schema.prisma new file mode 100644 index 0000000..8582c5d --- /dev/null +++ b/apps/cli/template/with-prisma-sqlite/packages/server/prisma/schema/schema.prisma @@ -0,0 +1,8 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "sqlite" + url = "file:../local.db" +} diff --git a/apps/cli/template/with-prisma-postgres/packages/server/prisma/schema.prisma b/apps/cli/template/with-prisma-sqlite/packages/server/prisma/schema/todo.prisma similarity index 51% rename from apps/cli/template/with-prisma-postgres/packages/server/prisma/schema.prisma rename to apps/cli/template/with-prisma-sqlite/packages/server/prisma/schema/todo.prisma index c1210cb..4f23d6f 100644 --- a/apps/cli/template/with-prisma-postgres/packages/server/prisma/schema.prisma +++ b/apps/cli/template/with-prisma-sqlite/packages/server/prisma/schema/todo.prisma @@ -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 diff --git a/apps/cli/template/with-prisma-sqlite/packages/server/src/routers/todo.ts b/apps/cli/template/with-prisma-sqlite/packages/server/src/routers/todo.ts deleted file mode 100644 index 5f2d3f9..0000000 --- a/apps/cli/template/with-prisma-sqlite/packages/server/src/routers/todo.ts +++ /dev/null @@ -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", - }); - } - }), -}); diff --git a/apps/web/README.md b/apps/web/README.md index 359d517..3c4f431 100644 --- a/apps/web/README.md +++ b/apps/web/README.md @@ -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 diff --git a/apps/web/src/app/(home)/_components/CodeContainer.tsx b/apps/web/src/app/(home)/_components/CodeContainer.tsx index 922b439..c57b897 100644 --- a/apps/web/src/app/(home)/_components/CodeContainer.tsx +++ b/apps/web/src/app/(home)/_components/CodeContainer.tsx @@ -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(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 && (
    - {( - Object.keys(commands) as Array< - "npm" | "yarn" | "pnpm" | "bun" - > - ).map((pm) => ( -
  • - -
  • - ))} + {(Object.keys(commands) as Array<"npm" | "pnpm" | "bun">).map( + (pm) => ( +
  • + +
  • + ), + )}
)} diff --git a/apps/web/src/app/(home)/_components/Icons.tsx b/apps/web/src/app/(home)/_components/Icons.tsx index 373f517..eb16d17 100644 --- a/apps/web/src/app/(home)/_components/Icons.tsx +++ b/apps/web/src/app/(home)/_components/Icons.tsx @@ -7,30 +7,6 @@ const PackageIcon = ({ pm, className }: { pm: string; className?: string }) => { ); - case "yarn": - return ( - - yarn - - - - ); case "pnpm": return ( = { 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: [