From b0e3432554a719936f1a76bac2a11bbc8de48463 Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Sat, 29 Mar 2025 18:09:36 +0530 Subject: [PATCH] several bug fixes --- .changeset/odd-months-move.md | 5 + apps/cli/package.json | 5 +- apps/cli/src/helpers/addons-setup.ts | 13 +- apps/cli/src/helpers/create-project.ts | 8 +- apps/cli/src/helpers/examples-setup.ts | 9 +- apps/cli/src/helpers/tauri-setup.ts | 4 + apps/cli/src/index.ts | 12 +- apps/cli/src/prompts/addons.ts | 62 +++-- apps/cli/src/prompts/config-prompts.ts | 4 +- apps/cli/src/prompts/examples.ts | 10 +- apps/cli/src/utils/display-config.ts | 6 + .../app/(home)/_components/StackArchitech.tsx | 231 ++++++++++++++++-- 12 files changed, 304 insertions(+), 65 deletions(-) create mode 100644 .changeset/odd-months-move.md diff --git a/.changeset/odd-months-move.md b/.changeset/odd-months-move.md new file mode 100644 index 0000000..fc443e2 --- /dev/null +++ b/.changeset/odd-months-move.md @@ -0,0 +1,5 @@ +--- +"create-better-t-stack": patch +--- + +fix several bugs diff --git a/apps/cli/package.json b/apps/cli/package.json index fdf88c0..db91ddd 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -7,10 +7,7 @@ "bin": { "create-better-t-stack": "dist/index.js" }, - "files": [ - "dist", - "template" - ], + "files": ["dist", "template"], "keywords": [], "repository": { "type": "git", diff --git a/apps/cli/src/helpers/addons-setup.ts b/apps/cli/src/helpers/addons-setup.ts index bba2912..97d219d 100644 --- a/apps/cli/src/helpers/addons-setup.ts +++ b/apps/cli/src/helpers/addons-setup.ts @@ -1,7 +1,7 @@ import path from "node:path"; import fs from "fs-extra"; import { PKG_ROOT } from "../constants"; -import type { PackageManager, ProjectAddons } from "../types"; +import type { PackageManager, ProjectAddons, ProjectFrontend } from "../types"; import { addPackageDependency } from "../utils/add-package-deps"; import { setupTauri } from "./tauri-setup"; @@ -9,14 +9,17 @@ export async function setupAddons( projectDir: string, addons: ProjectAddons[], packageManager: PackageManager, + frontends: ProjectFrontend[], ) { + const hasWebFrontend = frontends.includes("web"); + // if (addons.includes("docker")) { // await setupDocker(projectDir); // } - if (addons.includes("pwa")) { + if (addons.includes("pwa") && hasWebFrontend) { await setupPwa(projectDir); } - if (addons.includes("tauri")) { + if (addons.includes("tauri") && hasWebFrontend) { await setupTauri(projectDir, packageManager); } if (addons.includes("biome")) { @@ -89,6 +92,10 @@ async function setupPwa(projectDir: string) { const clientPackageDir = path.join(projectDir, "apps/web"); + if (!(await fs.pathExists(clientPackageDir))) { + return; + } + addPackageDependency({ dependencies: ["vite-plugin-pwa"], devDependencies: ["@vite-pwa/assets-generator"], diff --git a/apps/cli/src/helpers/create-project.ts b/apps/cli/src/helpers/create-project.ts index 9d988c5..98d6358 100644 --- a/apps/cli/src/helpers/create-project.ts +++ b/apps/cli/src/helpers/create-project.ts @@ -71,6 +71,7 @@ export async function createProject(options: ProjectConfig): Promise { options.examples, options.orm, options.auth, + options.frontend, ); await setupEnvironmentVariables(projectDir, options); @@ -78,7 +79,12 @@ export async function createProject(options: ProjectConfig): Promise { await initializeGit(projectDir, options.git); if (options.addons.length > 0) { - await setupAddons(projectDir, options.addons, options.packageManager); + await setupAddons( + projectDir, + options.addons, + options.packageManager, + options.frontend, + ); } await updatePackageConfigurations(projectDir, options); diff --git a/apps/cli/src/helpers/examples-setup.ts b/apps/cli/src/helpers/examples-setup.ts index f9661d5..15783eb 100644 --- a/apps/cli/src/helpers/examples-setup.ts +++ b/apps/cli/src/helpers/examples-setup.ts @@ -1,15 +1,20 @@ import path from "node:path"; import fs from "fs-extra"; import { PKG_ROOT } from "../constants"; -import type { ProjectOrm } from "../types"; +import type { ProjectFrontend, ProjectOrm } from "../types"; export async function setupExamples( projectDir: string, examples: string[], orm: ProjectOrm, auth: boolean, + frontend: ProjectFrontend[] = ["web"], ): Promise { - if (examples.includes("todo")) { + const hasWebFrontend = frontend.includes("web"); + + const webAppExists = await fs.pathExists(path.join(projectDir, "apps/web")); + + if (examples.includes("todo") && hasWebFrontend && webAppExists) { await setupTodoExample(projectDir, orm, auth); } else { await cleanupTodoFiles(projectDir, orm); diff --git a/apps/cli/src/helpers/tauri-setup.ts b/apps/cli/src/helpers/tauri-setup.ts index f590792..a28b533 100644 --- a/apps/cli/src/helpers/tauri-setup.ts +++ b/apps/cli/src/helpers/tauri-setup.ts @@ -13,6 +13,10 @@ export async function setupTauri( const s = spinner(); const clientPackageDir = path.join(projectDir, "apps/web"); + if (!(await fs.pathExists(clientPackageDir))) { + return; + } + try { s.start("Setting up Tauri desktop app support..."); diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts index c564181..220cdcc 100644 --- a/apps/cli/src/index.ts +++ b/apps/cli/src/index.ts @@ -122,13 +122,13 @@ async function main() { }), ...((options.web !== undefined || options.native !== undefined) && { frontend: [ - ...(options.web === false ? [] : options.web === true ? ["web"] : []), + ...(options.web === false ? [] : ["web"]), ...(options.native === false ? [] : options.native === true ? ["native"] : []), - ] as ProjectFrontend[], + ].filter(Boolean) as ProjectFrontend[], }), }; @@ -137,7 +137,6 @@ async function main() { log.message(displayConfig(flagConfig)); log.message(""); } - const config = options.yes ? { ...DEFAULT_CONFIG, @@ -180,6 +179,13 @@ async function main() { runtime: options.runtime ? (options.runtime as Runtime) : DEFAULT_CONFIG.runtime, + frontend: + options.web === false || options.native === true + ? ([ + ...(options.web === false ? [] : ["web"]), + ...(options.native ? ["native"] : []), + ] as ProjectFrontend[]) + : DEFAULT_CONFIG.frontend, } : await gatherConfig(flagConfig); diff --git a/apps/cli/src/prompts/addons.ts b/apps/cli/src/prompts/addons.ts index 17f9c37..86ad152 100644 --- a/apps/cli/src/prompts/addons.ts +++ b/apps/cli/src/prompts/addons.ts @@ -1,38 +1,52 @@ import { cancel, isCancel, multiselect } from "@clack/prompts"; import pc from "picocolors"; import { DEFAULT_CONFIG } from "../constants"; -import type { ProjectAddons } from "../types"; +import type { ProjectAddons, ProjectFrontend } from "../types"; export async function getAddonsChoice( Addons?: ProjectAddons[], + frontends?: ProjectFrontend[], ): Promise { if (Addons !== undefined) return Addons; + const hasWeb = frontends?.includes("web"); + + const addonOptions = [ + { + value: "biome" as const, + label: "Biome", + hint: "Add Biome for linting and formatting", + }, + { + value: "husky" as const, + label: "Husky", + hint: "Add Git hooks with Husky, lint-staged (requires Biome)", + }, + ]; + + const webAddonOptions = [ + { + value: "pwa" as const, + label: "PWA (Progressive Web App)", + hint: "Make your app installable and work offline", + }, + { + value: "tauri" as const, + label: "Tauri Desktop App", + hint: "Build native desktop apps from your web frontend", + }, + ]; + + const options = hasWeb ? [...webAddonOptions, ...addonOptions] : addonOptions; + + const initialValues = DEFAULT_CONFIG.addons.filter( + (addon) => hasWeb || (addon !== "pwa" && addon !== "tauri"), + ); + const response = await multiselect({ message: "Which Addons would you like to add?", - options: [ - { - value: "pwa", - label: "PWA (Progressive Web App)", - hint: "Make your app installable and work offline", - }, - { - value: "tauri", - label: "Tauri Desktop App", - hint: "Build native desktop apps from your web frontend", - }, - { - value: "biome", - label: "Biome", - hint: "Add Biome for linting and formatting", - }, - { - value: "husky", - label: "Husky", - hint: "Add Git hooks with Husky, lint-staged (requires Biome)", - }, - ], - initialValues: DEFAULT_CONFIG.addons, + options, + initialValues, required: false, }); diff --git a/apps/cli/src/prompts/config-prompts.ts b/apps/cli/src/prompts/config-prompts.ts index a6706c6..27113cc 100644 --- a/apps/cli/src/prompts/config-prompts.ts +++ b/apps/cli/src/prompts/config-prompts.ts @@ -65,9 +65,9 @@ export async function gatherConfig( results.database === "sqlite" && results.orm !== "prisma" ? getTursoSetupChoice(flags.turso) : Promise.resolve(false), - addons: () => getAddonsChoice(flags.addons), + addons: ({ results }) => getAddonsChoice(flags.addons, results.frontend), examples: ({ results }) => - getExamplesChoice(flags.examples, results.database), + getExamplesChoice(flags.examples, results.database, results.frontend), git: () => getGitChoice(flags.git), packageManager: () => getPackageManagerChoice(flags.packageManager), noInstall: () => getNoInstallChoice(flags.noInstall), diff --git a/apps/cli/src/prompts/examples.ts b/apps/cli/src/prompts/examples.ts index 40629f5..7fd0b4e 100644 --- a/apps/cli/src/prompts/examples.ts +++ b/apps/cli/src/prompts/examples.ts @@ -1,16 +1,24 @@ import { cancel, isCancel, multiselect } from "@clack/prompts"; import pc from "picocolors"; import { DEFAULT_CONFIG } from "../constants"; -import type { ProjectDatabase, ProjectExamples } from "../types"; +import type { + ProjectDatabase, + ProjectExamples, + ProjectFrontend, +} from "../types"; export async function getExamplesChoice( examples?: ProjectExamples[], database?: ProjectDatabase, + frontends?: ProjectFrontend[], ): Promise { if (examples !== undefined) return examples; if (database === "none") return []; + const hasWebFrontend = frontends?.includes("web"); + if (!hasWebFrontend) return []; + const response = await multiselect({ message: "Which examples would you like to include?", options: [ diff --git a/apps/cli/src/utils/display-config.ts b/apps/cli/src/utils/display-config.ts index 1e502c9..6a6dcb3 100644 --- a/apps/cli/src/utils/display-config.ts +++ b/apps/cli/src/utils/display-config.ts @@ -8,6 +8,12 @@ export function displayConfig(config: Partial) { configDisplay.push(`${pc.blue("Project Name:")} ${config.projectName}`); } + if (config.frontend !== undefined) { + const frontendText = + config.frontend.length > 0 ? config.frontend.join(", ") : "none"; + configDisplay.push(`${pc.blue("Frontend:")} ${frontendText}`); + } + if (config.backendFramework !== undefined) { configDisplay.push( `${pc.blue("Backend Framework:")} ${config.backendFramework}`, diff --git a/apps/web/src/app/(home)/_components/StackArchitech.tsx b/apps/web/src/app/(home)/_components/StackArchitech.tsx index 03c2ff4..0599f91 100644 --- a/apps/web/src/app/(home)/_components/StackArchitech.tsx +++ b/apps/web/src/app/(home)/_components/StackArchitech.tsx @@ -6,6 +6,7 @@ import { Circle, CircleCheck, ClipboardCopy, + InfoIcon, Terminal, } from "lucide-react"; import { useCallback, useEffect, useState } from "react"; @@ -41,7 +42,7 @@ const triggerConfetti = () => { const animate = () => { posX += vx; posY += vy; - vy += 0.1; // Gravity + vy += 0.1; opacity -= 0.01; rotation += 5; @@ -68,6 +69,31 @@ const triggerConfetti = () => { } }; +const validateProjectName = (name: string): string | undefined => { + const INVALID_CHARS = ["<", ">", ":", '"', "|", "?", "*"]; + const MAX_LENGTH = 255; + + if (name === ".") return undefined; + + if (!name) return "Project name cannot be empty"; + if (name.length > MAX_LENGTH) { + return `Project name must be less than ${MAX_LENGTH} characters`; + } + if (INVALID_CHARS.some((char) => name.includes(char))) { + return "Project name contains invalid characters"; + } + if (name.startsWith(".") || name.startsWith("-")) { + return "Project name cannot start with a dot or dash"; + } + if ( + name.toLowerCase() === "node_modules" || + name.toLowerCase() === "favicon.ico" + ) { + return "Project name is reserved"; + } + return undefined; +}; + const TECH_OPTIONS = { frontend: [ { @@ -310,6 +336,7 @@ const TECH_OPTIONS = { }; interface StackState { + projectName: string; frontend: string[]; runtime: string; backendFramework: string; @@ -325,6 +352,7 @@ interface StackState { } const DEFAULT_STACK: StackState = { + projectName: "my-better-t-app", frontend: ["web"], runtime: "bun", backendFramework: "hono", @@ -341,13 +369,65 @@ const DEFAULT_STACK: StackState = { const StackArchitect = () => { const [stack, setStack] = useState(DEFAULT_STACK); - const [command, setCommand] = useState("npx create-better-t-stack my-app -y"); + const [command, setCommand] = useState( + "npx create-better-t-stack my-better-t-app --yes", + ); const [activeTab, setActiveTab] = useState("frontend"); const [copied, setCopied] = useState(false); + const [compatNotes, setCompatNotes] = useState>({}); + const [projectNameError, setProjectNameError] = useState( + undefined, + ); + + useEffect(() => { + if (!stack.frontend.includes("web") && stack.auth === "true") { + setStack((prev) => ({ + ...prev, + auth: "false", + })); + } + }, [stack.frontend, stack.auth]); useEffect(() => { const cmd = generateCommand(stack); setCommand(cmd); + + const notes: Record = {}; + + notes.frontend = []; + + notes.auth = []; + if (!stack.frontend.includes("web") && stack.auth === "true") { + notes.auth.push("Authentication is only available with React Web."); + } + + notes.addons = []; + if (!stack.frontend.includes("web")) { + notes.addons.push("PWA and Tauri are only available with React Web."); + } + + notes.database = []; + + notes.orm = []; + if (stack.database === "none") { + notes.orm.push( + "ORM options are only available when a database is selected.", + ); + } + + notes.turso = []; + if (stack.database !== "sqlite") { + notes.turso.push( + "Turso integration is only available with SQLite database.", + ); + } + + notes.examples = []; + if (!stack.frontend.includes("web")) { + notes.examples.push("Todo example is only available with React Web."); + } + + setCompatNotes(notes); }, [stack]); const generateCommand = useCallback((stackState: StackState) => { @@ -360,7 +440,7 @@ const StackArchitect = () => { base = "bun create better-t-stack@latest"; } - const projectName = "my-better-t-app"; + const projectName = stackState.projectName || "my-better-t-app"; const flags: string[] = []; const isAllDefault = @@ -452,39 +532,80 @@ const StackArchitect = () => { if (techId === "none") { return { ...prev, - frontend: [], + frontend: ["none"], auth: "false", + examples: prev.examples.filter((ex) => ex !== "todo"), + addons: prev.addons.filter( + (addon) => addon !== "pwa" && addon !== "tauri", + ), }; } if (currentSelection.includes(techId)) { - if ( - techId === "web" && - currentSelection.filter((id) => id !== techId).length === 0 - ) { + if (techId === "web") { + const newFrontend = currentSelection.filter( + (id) => id !== techId, + ); + + if (newFrontend.length === 0) { + return { + ...prev, + frontend: ["none"], + auth: "false", + examples: prev.examples.filter((ex) => ex !== "todo"), + addons: prev.addons.filter( + (addon) => addon !== "pwa" && addon !== "tauri", + ), + }; + } + return { ...prev, - frontend: currentSelection.filter((id) => id !== techId), + frontend: newFrontend, auth: "false", + examples: prev.examples.filter((ex) => ex !== "todo"), + addons: prev.addons.filter( + (addon) => addon !== "pwa" && addon !== "tauri", + ), }; } + + const newFrontend = currentSelection.filter((id) => id !== techId); + + if (newFrontend.length === 0) { + return { + ...prev, + frontend: ["none"], + auth: "false", + addons: prev.addons.filter( + (addon) => addon !== "pwa" && addon !== "tauri", + ), + }; + } + return { ...prev, - frontend: currentSelection.filter((id) => id !== techId), + frontend: newFrontend, }; } if (techId === "web") { + const cleanedSelection = currentSelection.filter( + (id) => id !== "none", + ); return { ...prev, - frontend: [...currentSelection, techId], + frontend: [...cleanedSelection, techId], auth: "true", }; } + const cleanedSelection = currentSelection.filter( + (id) => id !== "none", + ); return { ...prev, - frontend: [...currentSelection, techId], + frontend: [...cleanedSelection, techId], }; } @@ -495,6 +616,20 @@ const StackArchitect = () => { if (index >= 0) { currentArray.splice(index, 1); } else { + if ( + category === "examples" && + techId === "todo" && + !prev.frontend.includes("web") + ) { + return prev; + } + if ( + category === "addons" && + (techId === "pwa" || techId === "tauri") && + !prev.frontend.includes("web") + ) { + return prev; + } currentArray.push(techId); } @@ -510,6 +645,7 @@ const StackArchitect = () => { ...prev, database: techId, orm: null, + turso: "false", }; } @@ -520,17 +656,15 @@ const StackArchitect = () => { orm: "drizzle", }; } - } - if (category === "database" && techId === "sqlite") { - return { - ...prev, - database: techId, - turso: prev.turso, - }; - } + if (techId === "sqlite") { + return { + ...prev, + database: techId, + turso: prev.turso, + }; + } - if (category === "database" && techId !== "sqlite") { return { ...prev, database: techId, @@ -538,6 +672,10 @@ const StackArchitect = () => { }; } + if (category === "turso" && prev.database !== "sqlite") { + return prev; + } + return { ...prev, [category]: techId, @@ -582,8 +720,34 @@ const StackArchitect = () => { -
+
+ +
$ @@ -592,7 +756,19 @@ const StackArchitect = () => {
- + {compatNotes[activeTab] && compatNotes[activeTab].length > 0 && ( +
+
+ + Compatibility Notes +
+
    + {compatNotes[activeTab].map((note) => ( +
  • {note}
  • + ))} +
+
+ )}
@@ -618,7 +794,13 @@ const StackArchitect = () => { const isDisabled = (activeTab === "orm" && stack.database === "none") || (activeTab === "turso" && stack.database !== "sqlite") || - (activeTab === "auth" && !stack.frontend.includes("web")); + (activeTab === "auth" && !stack.frontend.includes("web")) || + (activeTab === "examples" && + tech.id === "todo" && + !stack.frontend.includes("web")) || + (activeTab === "addons" && + (tech.id === "pwa" || tech.id === "tauri") && + !stack.frontend.includes("web")); return ( {
-
{Object.keys(TECH_OPTIONS).map((category) => (