diff --git a/apps/web/public/icon/solid.svg b/apps/web/public/icon/solid.svg new file mode 100644 index 0000000..dd92ac8 --- /dev/null +++ b/apps/web/public/icon/solid.svg @@ -0,0 +1 @@ + diff --git a/apps/web/src/app/(home)/_components/StackArchitech.tsx b/apps/web/src/app/(home)/_components/StackArchitech.tsx index a7d3db3..404ca9c 100644 --- a/apps/web/src/app/(home)/_components/StackArchitech.tsx +++ b/apps/web/src/app/(home)/_components/StackArchitech.tsx @@ -13,6 +13,7 @@ import { PRESET_TEMPLATES, type StackState, TECH_OPTIONS, + isStackDefault, } from "@/lib/constant"; import { stackParsers, stackQueryStatesOptions } from "@/lib/stack-url-state"; import { cn } from "@/lib/utils"; @@ -87,17 +88,20 @@ const hasWebFrontend = (frontend: string[]) => "next", "nuxt", "svelte", + "solid", ].includes(f), ); const hasNativeFrontend = (frontend: string[]) => frontend.includes("native"); const hasPWACompatibleFrontend = (frontend: string[]) => - frontend.some((f) => ["tanstack-router", "react-router"].includes(f)); + frontend.some((f) => + ["tanstack-router", "react-router", "solid"].includes(f), + ); const hasTauriCompatibleFrontend = (frontend: string[]) => frontend.some((f) => - ["tanstack-router", "react-router", "nuxt", "svelte"].includes(f), + ["tanstack-router", "react-router", "nuxt", "svelte", "solid"].includes(f), ); const getBadgeColors = (category: string): string => { @@ -215,6 +219,34 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => { }); } } + const incompatibleConvexFrontends = ["nuxt", "solid"]; + const originalFrontendLength = nextStack.frontend.length; + nextStack.frontend = nextStack.frontend.filter( + (f) => !incompatibleConvexFrontends.includes(f), + ); + if (nextStack.frontend.length !== originalFrontendLength) { + changed = true; + notes.frontend.notes.push( + "Nuxt and Solid are not compatible with Convex backend and have been removed.", + ); + notes.backend.notes.push( + "Convex backend is not compatible with Nuxt or Solid.", + ); + notes.frontend.hasIssue = true; + notes.backend.hasIssue = true; + changes.push({ + category: "convex", + message: "Removed incompatible frontends (Nuxt, Solid)", + }); + } + if (nextStack.frontend.length === 0) { + nextStack.frontend = ["tanstack-router"]; + changed = true; + changes.push({ + category: "convex", + message: "Frontend defaulted to TanStack Router", + }); + } } else { if (nextStack.runtime === "none") { notes.runtime.notes.push( @@ -310,18 +342,18 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => { } else { if (nextStack.orm === "mongoose") { notes.database.notes.push( - "Relational databases are not compatible with Mongoose ORM", + "Relational databases are not compatible with Mongoose ORM. Defaulting to Drizzle.", ); notes.orm.notes.push( - "Relational databases are not compatible with Mongoose ORM", + "Mongoose ORM only works with MongoDB. Defaulting to Drizzle.", ); notes.database.hasIssue = true; notes.orm.hasIssue = true; - nextStack.orm = "prisma"; + nextStack.orm = "drizzle"; changed = true; changes.push({ category: "database", - message: "ORM set to 'Prisma' (Mongoose only works with MongoDB)", + message: "ORM set to 'Drizzle' (Mongoose only works with MongoDB)", }); } if (nextStack.dbSetup === "mongodb-atlas") { @@ -457,8 +489,9 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => { const isNuxt = nextStack.frontend.includes("nuxt"); const isSvelte = nextStack.frontend.includes("svelte"); - if ((isNuxt || isSvelte) && nextStack.api === "trpc") { - const frontendName = isNuxt ? "Nuxt" : "Svelte"; + const isSolid = nextStack.frontend.includes("solid"); + if ((isNuxt || isSvelte || isSolid) && nextStack.api === "trpc") { + const frontendName = isNuxt ? "Nuxt" : isSvelte ? "Svelte" : "Solid"; notes.api.notes.push( `${frontendName} requires oRPC. It will be selected automatically.`, ); @@ -482,25 +515,25 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => { if (!isPWACompat && nextStack.addons.includes("pwa")) { incompatibleAddons.push("pwa"); notes.frontend.notes.push( - "PWA addon requires TanStack or React Router. Addon will be removed.", + "PWA addon requires TanStack/React Router or Solid. Addon will be removed.", ); notes.addons.notes.push( - "PWA requires TanStack/React Router. It will be removed.", + "PWA requires TanStack/React Router/Solid. It will be removed.", ); notes.frontend.hasIssue = true; notes.addons.hasIssue = true; changes.push({ category: "addons", - message: "PWA addon removed (requires TanStack or React Router)", + message: "PWA addon removed (requires compatible frontend)", }); } if (!isTauriCompat && nextStack.addons.includes("tauri")) { incompatibleAddons.push("tauri"); notes.frontend.notes.push( - "Tauri addon requires TanStack Router, React Router, Nuxt or Svelte. Addon will be removed.", + "Tauri addon requires TanStack/React Router, Nuxt, Svelte or Solid. Addon will be removed.", ); notes.addons.notes.push( - "Tauri requires TanStack/React Router/Nuxt/Svelte. It will be removed.", + "Tauri requires TanStack/React Router/Nuxt/Svelte/Solid. It will be removed.", ); notes.frontend.hasIssue = true; notes.addons.hasIssue = true; @@ -529,8 +562,7 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => { const incompatibleExamples: string[] = []; const isWeb = hasWebFrontend(nextStack.frontend); - const isNativeOnly = - hasNativeFrontend(nextStack.frontend) && !isWeb && !isConvex; + const isNativeOnly = hasNativeFrontend(nextStack.frontend) && !isWeb; if (isNativeOnly) { if (nextStack.examples.length > 0) { @@ -538,7 +570,7 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => { "Examples are not supported with Native-only frontend. Examples will be removed.", ); notes.examples.notes.push( - "Examples require a web frontend or Convex backend. They will be removed.", + "Examples require a web frontend. They will be removed.", ); notes.frontend.hasIssue = true; notes.examples.hasIssue = true; @@ -582,23 +614,31 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => { message: "AI example removed (not compatible with Elysia)", }); } + if (isSolid && nextStack.examples.includes("ai")) { + incompatibleExamples.push("ai"); + changes.push({ + category: "examples", + message: "AI example removed (not compatible with Solid)", + }); + } } const uniqueIncompatibleExamples = [...new Set(incompatibleExamples)]; if (uniqueIncompatibleExamples.length > 0) { - if ( - !isWeb && - (uniqueIncompatibleExamples.includes("todo") || - uniqueIncompatibleExamples.includes("ai")) - ) { - notes.frontend.notes.push( - "Examples require a web frontend. Incompatible examples will be removed.", - ); - notes.examples.notes.push( - "Requires a web frontend. Incompatible examples will be removed.", - ); - notes.frontend.hasIssue = true; - notes.examples.hasIssue = true; + if (!isWeb && !isNativeOnly) { + if ( + uniqueIncompatibleExamples.includes("todo") || + uniqueIncompatibleExamples.includes("ai") + ) { + notes.frontend.notes.push( + "Examples require a web frontend. Incompatible examples will be removed.", + ); + notes.examples.notes.push( + "Requires a web frontend. Incompatible examples will be removed.", + ); + notes.frontend.hasIssue = true; + notes.examples.hasIssue = true; + } } if ( nextStack.database === "none" && @@ -626,6 +666,16 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => { notes.backend.hasIssue = true; notes.examples.hasIssue = true; } + if (isSolid && uniqueIncompatibleExamples.includes("ai")) { + notes.frontend.notes.push( + "AI example is not compatible with Solid. It will be removed.", + ); + notes.examples.notes.push( + "AI example is not compatible with Solid. It will be removed.", + ); + notes.frontend.hasIssue = true; + notes.examples.hasIssue = true; + } const originalExamplesLength = nextStack.examples.length; nextStack.examples = nextStack.examples.filter( @@ -647,6 +697,9 @@ const getCompatibilityRules = (stack: StackState) => { const hasWebFrontendSelected = hasWebFrontend(stack.frontend); const hasNativeOnly = hasNativeFrontend(stack.frontend) && !hasWebFrontendSelected; + const hasSolid = stack.frontend.includes("solid"); + const hasNuxt = stack.frontend.includes("nuxt"); + const hasSvelte = stack.frontend.includes("svelte"); return { isConvex, @@ -655,8 +708,10 @@ const getCompatibilityRules = (stack: StackState) => { hasNativeOnly, hasPWACompatible: hasPWACompatibleFrontend(stack.frontend), hasTauriCompatible: hasTauriCompatibleFrontend(stack.frontend), - hasNuxtOrSvelte: - stack.frontend.includes("nuxt") || stack.frontend.includes("svelte"), + hasNuxtOrSvelteOrSolid: hasNuxt || hasSvelte || hasSolid, + hasSolid, + hasNuxt, + hasSvelte, }; }; @@ -675,42 +730,14 @@ const generateCommand = (stackState: StackState): string => { } const projectName = stackState.projectName || "my-better-t-app"; - const flags: string[] = ["--yes"]; + const flags: string[] = []; - const isDefault = ( + const checkDefault = ( key: K, value: StackState[K], - ) => { - const defaultValue = DEFAULT_STACK[key]; + ) => isStackDefault(stackState, key, value); - if (stackState.backend === "convex") { - if (key === "runtime" && value === "none") return true; - if (key === "database" && value === "none") return true; - if (key === "orm" && value === "none") return true; - if (key === "api" && value === "none") return true; - if (key === "auth" && value === "false") return true; - if (key === "dbSetup" && value === "none") return true; - if ( - key === "examples" && - Array.isArray(value) && - value.length === 1 && - value[0] === "todo" - ) - return true; - } - - if (Array.isArray(defaultValue) && Array.isArray(value)) { - const sortedDefault = [...defaultValue].sort(); - const sortedValue = [...value].sort(); - return ( - sortedDefault.length === sortedValue.length && - sortedDefault.every((item, index) => item === sortedValue[index]) - ); - } - return defaultValue === value; - }; - - if (!isDefault("frontend", stackState.frontend)) { + if (!checkDefault("frontend", stackState.frontend)) { if (stackState.frontend.length === 0 || stackState.frontend[0] === "none") { flags.push("--frontend none"); } else { @@ -718,51 +745,51 @@ const generateCommand = (stackState: StackState): string => { } } - if (!isDefault("backend", stackState.backend)) { + if (!checkDefault("backend", stackState.backend)) { flags.push(`--backend ${stackState.backend}`); } if (stackState.backend !== "convex") { - if (!isDefault("runtime", stackState.runtime)) { + if (!checkDefault("runtime", stackState.runtime)) { flags.push(`--runtime ${stackState.runtime}`); } - if (!isDefault("api", stackState.api)) { + if (!checkDefault("api", stackState.api)) { flags.push(`--api ${stackState.api}`); } - if (!isDefault("database", stackState.database)) { + if (!checkDefault("database", stackState.database)) { flags.push(`--database ${stackState.database}`); } - if (!isDefault("orm", stackState.orm)) { + if (!checkDefault("orm", stackState.orm)) { flags.push(`--orm ${stackState.orm}`); } - if (!isDefault("auth", stackState.auth)) { + if (!checkDefault("auth", stackState.auth)) { if (stackState.auth === "false" && DEFAULT_STACK.auth === "true") { flags.push("--no-auth"); } } - if (!isDefault("dbSetup", stackState.dbSetup)) { + if (!checkDefault("dbSetup", stackState.dbSetup)) { flags.push(`--db-setup ${stackState.dbSetup}`); } } else { - if (stackState.auth === "false" && DEFAULT_STACK.auth === "true") { - if (DEFAULT_STACK.auth === "true") { - } - } } - if (!isDefault("packageManager", stackState.packageManager)) { + if (!checkDefault("packageManager", stackState.packageManager)) { flags.push(`--package-manager ${stackState.packageManager}`); } - if (!isDefault("git", stackState.git)) { - if (stackState.git === "false") flags.push("--no-git"); + if (!checkDefault("git", stackState.git)) { + if (stackState.git === "false" && DEFAULT_STACK.git === "true") { + flags.push("--no-git"); + } } - if (!isDefault("install", stackState.install)) { - if (stackState.install === "false") flags.push("--no-install"); + if (!checkDefault("install", stackState.install)) { + if (stackState.install === "false" && DEFAULT_STACK.install === "true") { + flags.push("--no-install"); + } } - if (!isDefault("addons", stackState.addons)) { + if (!checkDefault("addons", stackState.addons)) { if (stackState.addons.length > 0) { flags.push(`--addons ${stackState.addons.join(" ")}`); } else { @@ -772,7 +799,7 @@ const generateCommand = (stackState: StackState): string => { } } - if (!isDefault("examples", stackState.examples)) { + if (!checkDefault("examples", stackState.examples)) { if (stackState.examples.length > 0) { flags.push(`--examples ${stackState.examples.join(" ")}`); } else { @@ -782,10 +809,6 @@ const generateCommand = (stackState: StackState): string => { } } - if (flags.length === 1 && flags[0] === "--yes") { - flags.pop(); - } - return `${base} ${projectName}${ flags.length > 0 ? ` ${flags.join(" ")}` : "" }`; @@ -842,7 +865,7 @@ const StackArchitect = () => { catKey, ) ) { - const convexDefaults: Record = { + const convexDefaults: Record = { runtime: "none", database: "none", orm: "none", @@ -860,7 +883,6 @@ const StackArchitect = () => { ); } } - if (catKey === "examples" && techId !== "todo") { addRule( category, @@ -868,6 +890,16 @@ const StackArchitect = () => { "Convex backend only supports the 'Todo' example.", ); } + if ( + catKey === "frontend" && + (techId === "nuxt" || techId === "solid") + ) { + addRule( + category, + techId, + `${tech.name} is not compatible with Convex backend.`, + ); + } continue; } @@ -887,11 +919,12 @@ const StackArchitect = () => { "API 'None' is only available with the Convex backend.", ); } - - if (techId === "trpc" && rules.hasNuxtOrSvelte) { - const frontendName = stack.frontend.includes("nuxt") + if (techId === "trpc" && rules.hasNuxtOrSvelteOrSolid) { + const frontendName = rules.hasNuxt ? "Nuxt" - : "Svelte"; + : rules.hasSvelte + ? "Svelte" + : "Solid"; addRule( category, techId, @@ -908,7 +941,6 @@ const StackArchitect = () => { "Select a database to enable ORM options.", ); } - if ( stack.database === "mongodb" && techId !== "prisma" && @@ -921,36 +953,31 @@ const StackArchitect = () => { "MongoDB requires the Prisma or Mongoose ORM.", ); } - if ( - stack.dbSetup === "turso" && - techId !== "drizzle" && - techId !== "none" + stack.database !== "mongodb" && + stack.database !== "none" && + techId === "mongoose" ) { addRule( category, techId, - "Turso DB setup requires the Drizzle ORM.", + "Mongoose ORM is only compatible with MongoDB.", ); } - - if ( - stack.dbSetup === "prisma-postgres" && - techId !== "prisma" && - techId !== "none" - ) { + if (stack.dbSetup === "turso" && techId !== "drizzle") { + addRule(category, techId, "Turso DB setup requires Drizzle ORM."); + } + if (stack.dbSetup === "prisma-postgres" && techId !== "prisma") { addRule( category, techId, "Prisma PostgreSQL setup requires Prisma ORM.", ); } - if ( stack.dbSetup === "mongodb-atlas" && techId !== "prisma" && - techId !== "mongoose" && - techId !== "none" + techId !== "mongoose" ) { addRule( category, @@ -958,7 +985,6 @@ const StackArchitect = () => { "MongoDB Atlas setup requires Prisma or Mongoose ORM.", ); } - if (techId === "none") { if (stack.database === "mongodb") { addRule( @@ -973,14 +999,13 @@ const StackArchitect = () => { if (stack.dbSetup === "prisma-postgres") { addRule(category, techId, "This DB setup requires Prisma ORM."); } - } - - if (techId === "mongoose" && stack.database !== "mongodb") { - addRule( - category, - techId, - "Mongoose ORM is not compatible with relational databases.", - ); + if (stack.dbSetup === "mongodb-atlas") { + addRule( + category, + techId, + "This DB setup requires Prisma or Mongoose ORM.", + ); + } } } @@ -991,36 +1016,32 @@ const StackArchitect = () => { techId, "Select a database before choosing a cloud setup.", ); - } - - if (techId === "turso") { - if (stack.database !== "sqlite" && stack.database !== "none") { - addRule(category, techId, "Turso requires SQLite database."); - } - if (stack.orm !== "drizzle" && stack.orm !== "none") { - addRule(category, techId, "Turso requires Drizzle ORM."); - } - } else if (techId === "prisma-postgres") { - if (stack.database !== "postgres" && stack.database !== "none") { - addRule(category, techId, "Requires PostgreSQL database."); - } - if (stack.orm !== "prisma" && stack.orm !== "none") { - addRule(category, techId, "Requires Prisma ORM."); - } - } else if (techId === "mongodb-atlas") { - if (stack.database !== "mongodb" && stack.database !== "none") { - addRule(category, techId, "Requires MongoDB database."); - } - if ( - stack.orm !== "prisma" && - stack.orm !== "mongoose" && - stack.orm !== "none" - ) { - addRule(category, techId, "Requires Prisma or Mongoose ORM."); - } - } else if (techId === "neon") { - if (stack.database !== "postgres" && stack.database !== "none") { - addRule(category, techId, "Requires PostgreSQL database."); + } else { + if (techId === "turso") { + if (stack.database !== "sqlite") { + addRule(category, techId, "Turso requires SQLite database."); + } + if (stack.orm !== "drizzle") { + addRule(category, techId, "Turso requires Drizzle ORM."); + } + } else if (techId === "prisma-postgres") { + if (stack.database !== "postgres") { + addRule(category, techId, "Requires PostgreSQL database."); + } + if (stack.orm !== "prisma") { + addRule(category, techId, "Requires Prisma ORM."); + } + } else if (techId === "mongodb-atlas") { + if (stack.database !== "mongodb") { + addRule(category, techId, "Requires MongoDB database."); + } + if (stack.orm !== "prisma" && stack.orm !== "mongoose") { + addRule(category, techId, "Requires Prisma or Mongoose ORM."); + } + } else if (techId === "neon") { + if (stack.database !== "postgres") { + addRule(category, techId, "Requires PostgreSQL database."); + } } } } @@ -1038,15 +1059,14 @@ const StackArchitect = () => { addRule( category, techId, - "Requires TanStack Router or React Router frontend.", + "Requires TanStack Router, React Router or Solid frontend.", ); } - if (techId === "tauri" && !rules.hasTauriCompatible) { addRule( category, techId, - "Requires TanStack Router, React Router, Nuxt or Svelte frontend.", + "Requires TanStack Router, React Router, Nuxt, Svelte or Solid frontend.", ); } } @@ -1059,27 +1079,31 @@ const StackArchitect = () => { "Examples are not supported with Native-only frontend.", ); } else { - if ( - (techId === "todo" || techId === "ai") && - !rules.hasWebFrontend - ) { + if (!rules.hasWebFrontend) { addRule( category, techId, "Requires a web frontend (TanStack Router, React Router, etc.).", ); } - if (techId === "todo" && stack.database === "none") { addRule(category, techId, "Todo example requires a database."); } - - if (techId === "ai" && stack.backend === "elysia") { - addRule( - category, - techId, - "AI example is not compatible with Elysia backend.", - ); + if (techId === "ai") { + if (stack.backend === "elysia") { + addRule( + category, + techId, + "AI example is not compatible with Elysia backend.", + ); + } + if (rules.hasSolid) { + addRule( + category, + techId, + "AI example is not compatible with Solid frontend.", + ); + } } } } @@ -1174,6 +1198,8 @@ const StackArchitect = () => { // biome-ignore lint/correctness/useExhaustiveDependencies: useEffect(() => { if (compatibilityAnalysis.adjustedStack) { + if (compatibilityAnalysis.changes.length > 0) { + } setLastChanges(compatibilityAnalysis.changes); setStack(compatibilityAnalysis.adjustedStack); } @@ -1202,7 +1228,9 @@ const StackArchitect = () => { catKey === "addons" || catKey === "examples" ) { - const currentArray = [...(currentValue as string[])]; + const currentArray = Array.isArray(currentValue) + ? [...currentValue] + : []; let nextArray = [...currentArray]; const isSelected = currentArray.includes(techId); @@ -1214,15 +1242,15 @@ const StackArchitect = () => { "next", "nuxt", "svelte", + "solid", ]; if (techId === "none") { nextArray = ["none"]; } else if (isSelected) { - if (currentArray.length > 1 || currentArray.includes("none")) { + if (currentArray.length > 1) { nextArray = nextArray.filter((id) => id !== techId); - if (nextArray.length === 0 && !currentArray.includes("none")) { - nextArray = ["none"]; - } + } else { + nextArray = ["none"]; } } else { nextArray = nextArray.filter((id) => id !== "none"); @@ -1583,30 +1611,7 @@ const StackArchitect = () => { TECH_OPTIONS[categoryKey as keyof typeof TECH_OPTIONS] || []; const categoryDisplayName = getCategoryDisplayName(categoryKey); - const filteredOptions = categoryOptions.filter((tech) => { - if ( - rules.isConvex && - tech.id === "none" && - ["runtime", "database", "orm", "api", "dbSetup"].includes( - categoryKey, - ) - ) { - return false; - } - if ( - rules.isConvex && - categoryKey === "auth" && - tech.id === "false" - ) { - return false; - } - if ( - rules.isConvex && - categoryKey === "examples" && - tech.id !== "todo" - ) { - return false; - } + const filteredOptions = categoryOptions.filter(() => { return true; }); @@ -1649,6 +1654,7 @@ const StackArchitect = () => { {filteredOptions.map((tech) => { let isSelected = false; const category = categoryKey as keyof StackState; + const currentValue = stack[category]; if ( category === "addons" || @@ -1656,10 +1662,10 @@ const StackArchitect = () => { category === "frontend" ) { isSelected = ( - (stack[category] as string[]) || [] + (currentValue as string[]) || [] ).includes(tech.id); } else { - isSelected = stack[category] === tech.id; + isSelected = currentValue === tech.id; } const disabledReason = disabledReasons.get( diff --git a/apps/web/src/lib/constant.ts b/apps/web/src/lib/constant.ts index df8f970..89037b2 100644 --- a/apps/web/src/lib/constant.ts +++ b/apps/web/src/lib/constant.ts @@ -66,6 +66,14 @@ export const TECH_OPTIONS = { color: "from-orange-500 to-orange-700", default: false, }, + { + id: "solid", + name: "Solid", + description: "Simple and performant reactivity for building UIs", + icon: "/icon/solid.svg", + color: "from-blue-600 to-blue-800", + default: false, + }, { id: "native", name: "React Native", @@ -417,7 +425,7 @@ export const PRESET_TEMPLATES = [ name: "Convex + React", description: "Reactive full-stack app with Convex and TanStack Router", stack: { - projectName: "my-convex-app", + projectName: "my-better-t-app", frontend: ["tanstack-router"], backend: "convex", runtime: "none", @@ -438,7 +446,7 @@ export const PRESET_TEMPLATES = [ name: "Mobile App", description: "React Native with Expo and SQLite database", stack: { - projectName: "my-native-app", + projectName: "my-better-t-app", frontend: ["native"], runtime: "bun", backend: "hono", @@ -459,7 +467,7 @@ export const PRESET_TEMPLATES = [ name: "API Only", description: "Backend API with Hono and PostgreSQL", stack: { - projectName: "my-api", + projectName: "my-better-t-app", frontend: ["none"], runtime: "bun", backend: "hono", @@ -480,7 +488,7 @@ export const PRESET_TEMPLATES = [ name: "Full Featured", description: "Complete setup with web, native, Turso, and addons", stack: { - projectName: "my-full-app", + projectName: "my-better-t-app", frontend: ["tanstack-router", "native"], runtime: "bun", backend: "hono", @@ -531,3 +539,37 @@ export const DEFAULT_STACK: StackState = { install: "true", api: "trpc", }; + +export const isStackDefault = ( + stack: StackState, + key: K, + value: StackState[K], +): boolean => { + const defaultValue = DEFAULT_STACK[key]; + + if (stack.backend === "convex") { + if (key === "runtime" && value === "none") return true; + if (key === "database" && value === "none") return true; + if (key === "orm" && value === "none") return true; + if (key === "api" && value === "none") return true; + if (key === "auth" && value === "false") return true; + if (key === "dbSetup" && value === "none") return true; + if ( + key === "examples" && + Array.isArray(value) && + value.length === 1 && + value[0] === "todo" + ) + return true; + } + + if (Array.isArray(defaultValue) && Array.isArray(value)) { + const sortedDefault = [...defaultValue].sort(); + const sortedValue = [...value].sort(); + return ( + sortedDefault.length === sortedValue.length && + sortedDefault.every((item, index) => item === sortedValue[index]) + ); + } + return defaultValue === value; +};