rename features to addons

This commit is contained in:
Aman Varshney
2025-03-06 14:35:01 +05:30
parent 3dbe289758
commit 5b13b04a20
17 changed files with 75 additions and 81 deletions

View File

@@ -0,0 +1,5 @@
---
"create-better-t-stack": patch
---
rename features to addons

View File

@@ -11,7 +11,7 @@ export const DEFAULT_CONFIG: ProjectConfig = {
database: "sqlite", database: "sqlite",
orm: "drizzle", orm: "drizzle",
auth: true, auth: true,
features: [], addons: [],
git: true, git: true,
packageManager: "npm", packageManager: "npm",
noInstall: false, noInstall: false,

View File

@@ -2,21 +2,18 @@ import path from "node:path";
import { log } from "@clack/prompts"; import { log } from "@clack/prompts";
import fs from "fs-extra"; import fs from "fs-extra";
import pc from "picocolors"; import pc from "picocolors";
import type { ProjectFeature } from "../types"; import type { ProjectAddons } from "../types";
export async function setupFeatures( export async function setupAddons(projectDir: string, addons: ProjectAddons[]) {
projectDir: string, if (addons.includes("docker")) {
features: ProjectFeature[],
) {
if (features.includes("docker")) {
await setupDocker(projectDir); await setupDocker(projectDir);
} }
if (features.includes("github-actions")) { if (addons.includes("github-actions")) {
await setupGithubActions(projectDir); await setupGithubActions(projectDir);
} }
if (features.includes("SEO")) { if (addons.includes("SEO")) {
log.info( log.info(
pc.yellow( pc.yellow(
"SEO feature is still a work-in-progress and will be available in a future update.", "SEO feature is still a work-in-progress and will be available in a future update.",

View File

@@ -5,10 +5,10 @@ import fs from "fs-extra";
import pc from "picocolors"; import pc from "picocolors";
import { PKG_ROOT } from "../constants"; import { PKG_ROOT } from "../constants";
import type { ProjectConfig } from "../types"; import type { ProjectConfig } from "../types";
import { setupAddons } from "./addons-setup";
import { configureAuth } from "./auth-setup"; import { configureAuth } from "./auth-setup";
import { createReadme } from "./create-readme"; import { createReadme } from "./create-readme";
import { setupDatabase } from "./db-setup"; import { setupDatabase } from "./db-setup";
import { setupFeatures } from "./feature-setup";
import { displayPostInstallInstructions } from "./post-installation"; import { displayPostInstallInstructions } from "./post-installation";
export async function createProject(options: ProjectConfig): Promise<string> { export async function createProject(options: ProjectConfig): Promise<string> {
@@ -92,8 +92,8 @@ export async function createProject(options: ProjectConfig): Promise<string> {
await $({ cwd: projectDir })`git init`; await $({ cwd: projectDir })`git init`;
} }
if (options.features.length > 0) { if (options.addons.length > 0) {
await setupFeatures(projectDir, options.features); await setupAddons(projectDir, options.addons);
} }
const packageJsonPath = path.join(projectDir, "package.json"); const packageJsonPath = path.join(projectDir, "package.json");

View File

@@ -19,7 +19,7 @@ function generateReadmeContent(options: ProjectConfig): string {
packageManager, packageManager,
database, database,
auth, auth,
features = [], addons = [],
orm = "drizzle", orm = "drizzle",
} = options; } = options;
@@ -32,7 +32,7 @@ This project was created with [Better-T-Stack](https://github.com/better-t-stack
## Features ## Features
${generateFeaturesList(database, auth, features, orm)} ${generateFeaturesList(database, auth, addons, orm)}
## Getting Started ## Getting Started

View File

@@ -15,7 +15,6 @@ export async function setupDatabase(
if (databaseType === "none") { if (databaseType === "none") {
await fs.remove(path.join(serverDir, "src/db")); await fs.remove(path.join(serverDir, "src/db"));
log.info(pc.yellow("Database configuration removed"));
return; return;
} }

View File

@@ -13,7 +13,6 @@ export async function installDependencies({
packageManager, packageManager,
}: InstallDependenciesOptions) { }: InstallDependenciesOptions) {
const s = spinner(); const s = spinner();
log.info(pc.blue(`Installing dependencies using ${packageManager}...`));
try { try {
s.start(`Running ${packageManager} install...`); s.start(`Running ${packageManager} install...`);
@@ -34,7 +33,7 @@ export async function installDependencies({
break; break;
} }
s.stop(pc.green("Dependencies installed successfully")); s.stop("Dependencies installed successfully");
} catch (error) { } catch (error) {
s.stop(pc.red("Failed to install dependencies")); s.stop(pc.red("Failed to install dependencies"));
if (error instanceof Error) { if (error instanceof Error) {

View File

@@ -5,7 +5,7 @@ import { DEFAULT_CONFIG } from "./constants";
import { createProject } from "./helpers/create-project"; import { createProject } from "./helpers/create-project";
import { installDependencies } from "./helpers/install-dependencies"; import { installDependencies } from "./helpers/install-dependencies";
import { gatherConfig } from "./prompts/config-prompts"; import { gatherConfig } from "./prompts/config-prompts";
import type { ProjectConfig, ProjectFeature } from "./types"; import type { ProjectAddons, ProjectConfig } from "./types";
import { displayConfig } from "./utils/display-config"; import { displayConfig } from "./utils/display-config";
import { generateReproducibleCommand } from "./utils/generate-reproducible-command"; import { generateReproducibleCommand } from "./utils/generate-reproducible-command";
import { getVersion } from "./utils/get-version"; import { getVersion } from "./utils/get-version";
@@ -33,7 +33,7 @@ async function main() {
.option("--docker", "Include Docker setup") .option("--docker", "Include Docker setup")
.option("--github-actions", "Include GitHub Actions") .option("--github-actions", "Include GitHub Actions")
.option("--seo", "Include SEO setup") .option("--seo", "Include SEO setup")
.option("--no-features", "Skip all additional features") .option("--no-addons", "Skip all additional addons")
.option("--git", "Include git setup") .option("--git", "Include git setup")
.option("--no-git", "Skip git initialization") .option("--no-git", "Skip git initialization")
.option("--npm", "Use npm package manager") .option("--npm", "Use npm package manager")
@@ -75,15 +75,15 @@ async function main() {
...((options.docker || ...((options.docker ||
options.githubActions || options.githubActions ||
options.seo || options.seo ||
options.features === false) && { options.addons === false) && {
features: addons:
options.features === false options.addons === false
? [] ? []
: ([ : ([
...(options.docker ? ["docker"] : []), ...(options.docker ? ["docker"] : []),
...(options.githubActions ? ["github-actions"] : []), ...(options.githubActions ? ["github-actions"] : []),
...(options.seo ? ["SEO"] : []), ...(options.seo ? ["SEO"] : []),
] as ProjectFeature[]), ] as ProjectAddons[]),
}), }),
}; };
@@ -117,9 +117,9 @@ async function main() {
: DEFAULT_CONFIG.noInstall, : DEFAULT_CONFIG.noInstall,
packageManager: packageManager:
flagConfig.packageManager ?? DEFAULT_CONFIG.packageManager, flagConfig.packageManager ?? DEFAULT_CONFIG.packageManager,
features: flagConfig.features?.length addons: flagConfig.addons?.length
? flagConfig.features ? flagConfig.addons
: DEFAULT_CONFIG.features, : DEFAULT_CONFIG.addons,
turso: turso:
"turso" in options "turso" in options
? options.turso ? options.turso

View File

@@ -1,14 +1,14 @@
import { cancel, isCancel, multiselect } from "@clack/prompts"; import { cancel, isCancel, multiselect } from "@clack/prompts";
import pc from "picocolors"; import pc from "picocolors";
import type { ProjectFeature } from "../types"; import type { ProjectAddons } from "../types";
export async function getFeaturesChoice( export async function getAddonsChoice(
features?: ProjectFeature[], Addons?: ProjectAddons[],
): Promise<ProjectFeature[]> { ): Promise<ProjectAddons[]> {
if (features !== undefined) return features; if (Addons !== undefined) return Addons;
const response = await multiselect<ProjectFeature>({ const response = await multiselect<ProjectAddons>({
message: "Which features would you like to add?", message: "Which Addons would you like to add?",
options: [ options: [
{ {
value: "docker", value: "docker",

View File

@@ -2,14 +2,14 @@ import { cancel, group } from "@clack/prompts";
import pc from "picocolors"; import pc from "picocolors";
import type { import type {
PackageManager, PackageManager,
ProjectAddons,
ProjectConfig, ProjectConfig,
ProjectDatabase, ProjectDatabase,
ProjectFeature,
ProjectOrm, ProjectOrm,
} from "../types"; } from "../types";
import { getAddonsChoice } from "./addons";
import { getAuthChoice } from "./auth"; import { getAuthChoice } from "./auth";
import { getDatabaseChoice } from "./database"; import { getDatabaseChoice } from "./database";
import { getFeaturesChoice } from "./features";
import { getGitChoice } from "./git"; import { getGitChoice } from "./git";
import { getNoInstallChoice } from "./install"; import { getNoInstallChoice } from "./install";
import { getORMChoice } from "./orm"; import { getORMChoice } from "./orm";
@@ -22,7 +22,7 @@ interface PromptGroupResults {
database: ProjectDatabase; database: ProjectDatabase;
orm: ProjectOrm; orm: ProjectOrm;
auth: boolean; auth: boolean;
features: ProjectFeature[]; addons: ProjectAddons[];
git: boolean; git: boolean;
packageManager: PackageManager; packageManager: PackageManager;
noInstall: boolean; noInstall: boolean;
@@ -46,7 +46,7 @@ export async function gatherConfig(
results.database === "sqlite" results.database === "sqlite"
? getTursoSetupChoice(flags.turso) ? getTursoSetupChoice(flags.turso)
: Promise.resolve(false), : Promise.resolve(false),
features: () => getFeaturesChoice(flags.features), addons: () => getAddonsChoice(flags.addons),
git: () => getGitChoice(flags.git), git: () => getGitChoice(flags.git),
packageManager: () => getPackageManagerChoice(flags.packageManager), packageManager: () => getPackageManagerChoice(flags.packageManager),
noInstall: () => getNoInstallChoice(flags.noInstall), noInstall: () => getNoInstallChoice(flags.noInstall),
@@ -64,7 +64,7 @@ export async function gatherConfig(
database: result.database, database: result.database,
orm: result.orm, orm: result.orm,
auth: result.auth, auth: result.auth,
features: result.features, addons: result.addons,
git: result.git, git: result.git,
packageManager: result.packageManager, packageManager: result.packageManager,
noInstall: result.noInstall, noInstall: result.noInstall,

View File

@@ -8,7 +8,7 @@ export async function getNoInstallChoice(
if (noInstall !== undefined) return noInstall; if (noInstall !== undefined) return noInstall;
const response = await confirm({ const response = await confirm({
message: "Install dependencies after creating project?", message: "Install dependencies?",
initialValue: !DEFAULT_CONFIG.noInstall, initialValue: !DEFAULT_CONFIG.noInstall,
}); });

View File

@@ -1,14 +1,14 @@
export type ProjectDatabase = "sqlite" | "postgres" | "none"; export type ProjectDatabase = "sqlite" | "postgres" | "none";
export type ProjectOrm = "drizzle" | "prisma" | "none"; export type ProjectOrm = "drizzle" | "prisma" | "none";
export type PackageManager = "npm" | "pnpm" | "yarn" | "bun"; export type PackageManager = "npm" | "pnpm" | "yarn" | "bun";
export type ProjectFeature = "docker" | "github-actions" | "SEO"; export type ProjectAddons = "docker" | "github-actions" | "SEO";
export interface ProjectConfig { export interface ProjectConfig {
projectName: string; projectName: string;
database: ProjectDatabase; database: ProjectDatabase;
orm: ProjectOrm; orm: ProjectOrm;
auth: boolean; auth: boolean;
features: ProjectFeature[]; addons: ProjectAddons[];
git: boolean; git: boolean;
packageManager: PackageManager; packageManager: PackageManager;
noInstall?: boolean; noInstall?: boolean;

View File

@@ -16,8 +16,8 @@ export function displayConfig(config: Partial<ProjectConfig>) {
if (config.auth !== undefined) { if (config.auth !== undefined) {
configDisplay.push(`${pc.blue("Authentication:")} ${config.auth}`); configDisplay.push(`${pc.blue("Authentication:")} ${config.auth}`);
} }
if (config.features?.length) { if (config.addons?.length) {
configDisplay.push(`${pc.blue("Features:")} ${config.features.join(", ")}`); configDisplay.push(`${pc.blue("Addons:")} ${config.addons.join(", ")}`);
} }
if (config.git !== undefined) { if (config.git !== undefined) {
configDisplay.push(`${pc.blue("Git Init:")} ${config.git}`); configDisplay.push(`${pc.blue("Git Init:")} ${config.git}`);

View File

@@ -41,12 +41,12 @@ export function generateReproducibleCommand(config: ProjectConfig): string {
flags.push(`--${config.packageManager}`); flags.push(`--${config.packageManager}`);
} }
if (config.features.length > 0) { if (config.addons.length > 0) {
for (const feature of config.features) { for (const addon of config.addons) {
flags.push(`--${feature}`); flags.push(`--${addon}`);
} }
} else { } else {
flags.push("--no-features"); flags.push("--no-addons");
} }
const baseCommand = "npx create-better-t-stack"; const baseCommand = "npx create-better-t-stack";

View File

@@ -175,7 +175,7 @@ const CodeContainer = () => {
<span className="text-yellow-400">yes</span> <span className="text-yellow-400">yes</span>
</p> </p>
<p className="text-white"> <p className="text-white">
Features:{" "} Addons:{" "}
<span className="text-yellow-400"> <span className="text-yellow-400">
docker, github-actions, SEO docker, github-actions, SEO
</span> </span>

View File

@@ -45,7 +45,7 @@ interface ActiveNodes {
orm: string; orm: string;
auth: string; auth: string;
packageManager: string; packageManager: string;
features: { addons: {
docker: boolean; docker: boolean;
githubActions: boolean; githubActions: boolean;
seo: boolean; seo: boolean;
@@ -62,7 +62,7 @@ const CustomizableStack = () => {
orm: "drizzle", orm: "drizzle",
auth: "better-auth", auth: "better-auth",
packageManager: "npm", packageManager: "npm",
features: { addons: {
docker: false, docker: false,
githubActions: false, githubActions: false,
seo: false, seo: false,
@@ -128,10 +128,10 @@ const CustomizableStack = () => {
setActiveNodes((prev) => ({ setActiveNodes((prev) => ({
...prev, ...prev,
[category]: techId, [category]: techId,
...(category === "features" && { ...(category === "addons" && {
features: { addons: {
...prev.features, ...prev.addons,
[techId]: !prev.features[techId as keyof typeof prev.features], [techId]: !prev.addons[techId as keyof typeof prev.addons],
}, },
}), }),
})); }));
@@ -342,16 +342,15 @@ const CustomizableStack = () => {
const command = "npx create-better-t-stack my-app"; const command = "npx create-better-t-stack my-app";
const flags: string[] = []; const flags: string[] = [];
// Check if all defaults are being used
const isAllDefaults = const isAllDefaults =
activeNodes.database === "sqlite" && activeNodes.database === "sqlite" &&
activeNodes.auth === "better-auth" && activeNodes.auth === "better-auth" &&
activeNodes.orm === "drizzle" && activeNodes.orm === "drizzle" &&
activeNodes.packageManager === "npm" && activeNodes.packageManager === "npm" &&
activeNodes.features.git === true && activeNodes.addons.git === true &&
!activeNodes.features.docker && !activeNodes.addons.docker &&
!activeNodes.features.githubActions && !activeNodes.addons.githubActions &&
!activeNodes.features.seo; !activeNodes.addons.seo;
// If using all defaults, just use -y flag // If using all defaults, just use -y flag
if (isAllDefaults) { if (isAllDefaults) {
@@ -387,19 +386,19 @@ const CustomizableStack = () => {
} }
// Feature flags // Feature flags
if (activeNodes.features.docker) { if (activeNodes.addons.docker) {
flags.push("--docker"); flags.push("--docker");
} }
if (activeNodes.features.githubActions) { if (activeNodes.addons.githubActions) {
flags.push("--github-actions"); flags.push("--github-actions");
} }
if (activeNodes.features.seo) { if (activeNodes.addons.seo) {
flags.push("--seo"); flags.push("--seo");
} }
if (!activeNodes.features.git) { if (!activeNodes.addons.git) {
flags.push("--no-git"); flags.push("--no-git");
} }
@@ -408,21 +407,17 @@ const CustomizableStack = () => {
return ( return (
<div className="relative w-full max-w-5xl mx-auto z-50 mt-24"> <div className="relative w-full max-w-5xl mx-auto z-50 mt-24">
{/* Command Display - Fixed at top with proper centering */}
<div className="absolute -top-16 left-0 right-0 mx-auto flex justify-center z-50"> <div className="absolute -top-16 left-0 right-0 mx-auto flex justify-center z-50">
<CommandDisplay command={command} /> <CommandDisplay command={command} />
</div> </div>
{/* Main container with proper layout */}
<div className="relative rounded-xl border border-gray-800 overflow-hidden"> <div className="relative rounded-xl border border-gray-800 overflow-hidden">
<div className="absolute inset-0 backdrop-blur-3xl bg-gradient-to-r from-blue-500/10 via-purple-500/10 to-pink-500/10" /> <div className="absolute inset-0 backdrop-blur-3xl bg-gradient-to-r from-blue-500/10 via-purple-500/10 to-pink-500/10" />
{/* Tech selector fixed to the left side */}
<div className="absolute left-0 top-0 bottom-0 lg:w-52 md:w-44 w-36 z-50 bg-gray-950/30 border-r border-gray-800/50"> <div className="absolute left-0 top-0 bottom-0 lg:w-52 md:w-44 w-36 z-50 bg-gray-950/30 border-r border-gray-800/50">
<TechSelector onSelect={handleTechSelect} activeNodes={activeNodes} /> <TechSelector onSelect={handleTechSelect} activeNodes={activeNodes} />
</div> </div>
{/* Help text */}
<div className="max-sm:hidden bg-gray-950/30 lg:p-4 p-1 absolute lg:top-4 top-2 lg:right-4 right-2 z-50 w-80 rounded-xl border border-gray-800 backdrop-blur-3xl"> <div className="max-sm:hidden bg-gray-950/30 lg:p-4 p-1 absolute lg:top-4 top-2 lg:right-4 right-2 z-50 w-80 rounded-xl border border-gray-800 backdrop-blur-3xl">
<div className="lg:text-sm text-xs text-gray-300 text-center"> <div className="lg:text-sm text-xs text-gray-300 text-center">
Select technologies from the left panel to customize your stack. The Select technologies from the left panel to customize your stack. The
@@ -430,7 +425,6 @@ const CustomizableStack = () => {
</div> </div>
</div> </div>
{/* Flow container with proper spacing from the selector */}
<div className="h-[600px] lg:pl-52 md:pl-44 pl-36 relative backdrop-blur-sm bg-gray-950/50"> <div className="h-[600px] lg:pl-52 md:pl-44 pl-36 relative backdrop-blur-sm bg-gray-950/50">
<ReactFlow <ReactFlow
nodes={nodes} nodes={nodes}

View File

@@ -4,7 +4,7 @@ interface ActiveNodes {
orm: string; orm: string;
auth: string; auth: string;
packageManager: string; packageManager: string;
features: { addons: {
docker: boolean; docker: boolean;
githubActions: boolean; githubActions: boolean;
seo: boolean; seo: boolean;
@@ -38,11 +38,11 @@ const techOptions: Record<string, TechOption[]> = {
{ id: "yarn", label: "Yarn", category: "packageManager" }, { id: "yarn", label: "Yarn", category: "packageManager" },
{ id: "bun", label: "Bun", category: "packageManager" }, { id: "bun", label: "Bun", category: "packageManager" },
], ],
features: [ addons: [
{ id: "docker", label: "Docker", category: "features" }, { id: "docker", label: "Docker", category: "addons" },
{ id: "githubActions", label: "GitHub Actions", category: "features" }, { id: "githubActions", label: "GitHub Actions", category: "addons" },
{ id: "seo", label: "SEO", category: "features" }, { id: "seo", label: "SEO", category: "addons" },
{ id: "git", label: "Git", category: "features" }, { id: "git", label: "Git", category: "addons" },
], ],
}; };
@@ -60,7 +60,7 @@ export function TechSelector({ onSelect, activeNodes }: TechSelectorProps) {
{/* Regular tech options */} {/* Regular tech options */}
{Object.entries(techOptions) {Object.entries(techOptions)
.filter(([category]) => category !== "features") .filter(([category]) => category !== "addons")
.map(([category, options]) => ( .map(([category, options]) => (
<div key={category} className="space-y-2"> <div key={category} className="space-y-2">
<div className="text-xs text-gray-400 capitalize">{category}</div> <div className="text-xs text-gray-400 capitalize">{category}</div>
@@ -71,7 +71,7 @@ export function TechSelector({ onSelect, activeNodes }: TechSelectorProps) {
variant="secondary" variant="secondary"
className={`cursor-pointer hover:bg-gray-700 ${ className={`cursor-pointer hover:bg-gray-700 ${
activeNodes[ activeNodes[
category as keyof Omit<ActiveNodes, "features"> category as keyof Omit<ActiveNodes, "addons">
] === option.id && "bg-blue-600 text-white" ] === option.id && "bg-blue-600 text-white"
}`} }`}
onClick={() => onSelect(category, option.id)} onClick={() => onSelect(category, option.id)}
@@ -85,18 +85,18 @@ export function TechSelector({ onSelect, activeNodes }: TechSelectorProps) {
{/* Feature toggles */} {/* Feature toggles */}
<div className="space-y-2"> <div className="space-y-2">
<div className="text-xs text-gray-400">Features</div> <div className="text-xs text-gray-400">Addons</div>
<div className="flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1">
{techOptions.features.map((option) => ( {techOptions.addons.map((option) => (
<Badge <Badge
key={option.id} key={option.id}
variant="secondary" variant="secondary"
className={`cursor-pointer hover:bg-gray-700 ${ className={`cursor-pointer hover:bg-gray-700 ${
activeNodes.features[ activeNodes.addons[
option.id as keyof typeof activeNodes.features option.id as keyof typeof activeNodes.addons
] === true && "bg-blue-600 text-white" ] === true && "bg-blue-600 text-white"
}`} }`}
onClick={() => onSelect("features", option.id)} onClick={() => onSelect("addons", option.id)}
> >
{option.label} {option.label}
</Badge> </Badge>