From ea3d8d97453d6c06d1e9d994016dd39434fdb21d Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Tue, 2 Sep 2025 01:56:09 +0530 Subject: [PATCH] fix(web): migrate auth from boolean to multi-option system --- apps/web/scripts/generate-analytics.ts | 16 +- .../app/(home)/_components/stack-builder.tsx | 7 +- .../analytics/_components/analytics-page.tsx | 4 +- .../analytics/_components/metrics-cards.tsx | 12 +- .../stack-configuration-charts.tsx | 12 +- .../app/(home)/analytics/_components/types.ts | 13 +- apps/web/src/app/(home)/new/page.tsx | 17 +- apps/web/src/lib/stack-utils.ts | 553 +++++++----------- 8 files changed, 254 insertions(+), 380 deletions(-) diff --git a/apps/web/scripts/generate-analytics.ts b/apps/web/scripts/generate-analytics.ts index 1a05ff1..97c6ca9 100644 --- a/apps/web/scripts/generate-analytics.ts +++ b/apps/web/scripts/generate-analytics.ts @@ -48,6 +48,7 @@ interface AggregatedAnalyticsData { totalProjects: number; avgProjectsPerDay: number; authEnabledPercent: number; + mostPopularAuth: string; mostPopularFrontend: string; mostPopularBackend: string; mostPopularORM: string; @@ -242,10 +243,18 @@ async function generateAnalyticsData() { cliVersionCounts[cliVersion] = (cliVersionCounts[cliVersion] || 0) + 1; - const auth = - row["*.properties.auth"] === "True" ? "enabled" : "disabled"; + // Handle both old boolean format and new string format + let auth: string; + const authValue = row["*.properties.auth"]; + if (authValue === "True" || authValue === "true") { + auth = "better-auth"; // Old format: true -> better-auth + } else if (authValue === "False" || authValue === "false") { + auth = "none"; // Old format: false -> none + } else { + auth = authValue || "none"; // New format: use actual value + } authCounts[auth] = (authCounts[auth] || 0) + 1; - if (auth === "enabled") authEnabledCount++; + if (auth !== "none") authEnabledCount++; const git = row["*.properties.git"] === "True" ? "enabled" : "disabled"; @@ -529,6 +538,7 @@ async function generateAnalyticsData() { totalProjects: totalRecords, avgProjectsPerDay, authEnabledPercent, + mostPopularAuth: getMostPopular(authCounts), mostPopularFrontend: getMostPopular(frontendCounts), mostPopularBackend: getMostPopular(backendCounts), mostPopularORM: getMostPopular(ormCounts), diff --git a/apps/web/src/app/(home)/_components/stack-builder.tsx b/apps/web/src/app/(home)/_components/stack-builder.tsx index 546c8f8..5335bbd 100644 --- a/apps/web/src/app/(home)/_components/stack-builder.tsx +++ b/apps/web/src/app/(home)/_components/stack-builder.tsx @@ -16,7 +16,6 @@ import { import { motion } from "motion/react"; import Image from "next/image"; import { useTheme } from "next-themes"; - import type React from "react"; import { startTransition, useEffect, useMemo, useRef, useState } from "react"; import { toast } from "sonner"; @@ -44,7 +43,7 @@ import { CATEGORY_ORDER, generateStackCommand, generateStackUrlFromState, - useStackStateWithAllParams, + useStackState, } from "@/lib/stack-utils"; import { cn } from "@/lib/utils"; @@ -1171,7 +1170,7 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => { }; const StackBuilder = () => { - const [stack, setStack] = useStackStateWithAllParams(); + const [stack, setStack] = useStackState(); const [command, setCommand] = useState(""); const [copied, setCopied] = useState(false); @@ -1383,7 +1382,7 @@ const StackBuilder = () => { } startTransition(() => { - setStack((currentStack) => { + setStack((currentStack: StackState) => { const catKey = category as keyof StackState; const update: Partial = {}; const currentValue = currentStack[catKey]; diff --git a/apps/web/src/app/(home)/analytics/_components/analytics-page.tsx b/apps/web/src/app/(home)/analytics/_components/analytics-page.tsx index 1ee4648..521bee6 100644 --- a/apps/web/src/app/(home)/analytics/_components/analytics-page.tsx +++ b/apps/web/src/app/(home)/analytics/_components/analytics-page.tsx @@ -15,13 +15,13 @@ export default function AnalyticsPage({ }) { const totalProjects = data?.summary?.totalProjects || 0; const avgProjectsPerDay = data?.summary?.avgProjectsPerDay || 0; - const authEnabledPercent = data?.summary?.authEnabledPercent || 0; const mostPopularFrontend = data?.summary?.mostPopularFrontend || "None"; const mostPopularBackend = data?.summary?.mostPopularBackend || "None"; const mostPopularORM = data?.summary?.mostPopularORM || "None"; const mostPopularAPI = data?.summary?.mostPopularAPI || "None"; const mostPopularPackageManager = data?.summary?.mostPopularPackageManager || "npm"; + const mostPopularAuth = data?.summary?.mostPopularAuth || "None"; return (
@@ -34,7 +34,7 @@ export default function AnalyticsPage({
- AUTH_ADOPTION + TOP_AUTH
-
- {authEnabledPercent}% +
+ {mostPopularAuth}

- $ auth_enabled_percentage.sh + $ most_selected_auth.sh

diff --git a/apps/web/src/app/(home)/analytics/_components/stack-configuration-charts.tsx b/apps/web/src/app/(home)/analytics/_components/stack-configuration-charts.tsx index 97074ef..0315510 100644 --- a/apps/web/src/app/(home)/analytics/_components/stack-configuration-charts.tsx +++ b/apps/web/src/app/(home)/analytics/_components/stack-configuration-charts.tsx @@ -412,10 +412,12 @@ export function StackConfigurationCharts({
- AUTH_ADOPTION.PIE + + AUTH_DISTRIBUTION.PIE +

- Authentication implementation rate + Authentication provider distribution

@@ -439,9 +441,11 @@ export function StackConfigurationCharts({ ))} diff --git a/apps/web/src/app/(home)/analytics/_components/types.ts b/apps/web/src/app/(home)/analytics/_components/types.ts index 35c606b..28f92d0 100644 --- a/apps/web/src/app/(home)/analytics/_components/types.ts +++ b/apps/web/src/app/(home)/analytics/_components/types.ts @@ -40,6 +40,7 @@ export interface AggregatedAnalyticsData { totalProjects: number; avgProjectsPerDay: number; authEnabledPercent: number; + mostPopularAuth: string; mostPopularFrontend: string; mostPopularBackend: string; mostPopularORM: string; @@ -284,14 +285,18 @@ export const cliVersionConfig = { } satisfies ChartConfig; export const authConfig = { - enabled: { - label: "Enabled", + "better-auth": { + label: "Better Auth", color: "hsl(var(--chart-1))", }, - disabled: { - label: "Disabled", + clerk: { + label: "Clerk", color: "hsl(var(--chart-2))", }, + none: { + label: "No Auth", + color: "hsl(var(--chart-3))", + }, } satisfies ChartConfig; export const gitConfig = { diff --git a/apps/web/src/app/(home)/new/page.tsx b/apps/web/src/app/(home)/new/page.tsx index b26cfb3..bec25c1 100644 --- a/apps/web/src/app/(home)/new/page.tsx +++ b/apps/web/src/app/(home)/new/page.tsx @@ -1,20 +1,9 @@ -"use client"; - -import { motion } from "motion/react"; -import { Suspense } from "react"; import StackBuilder from "../_components/stack-builder"; export default function FullScreenStackBuilder() { return ( - - - - - +
+ +
); } diff --git a/apps/web/src/lib/stack-utils.ts b/apps/web/src/lib/stack-utils.ts index be15efd..9928e27 100644 --- a/apps/web/src/lib/stack-utils.ts +++ b/apps/web/src/lib/stack-utils.ts @@ -17,10 +17,6 @@ import { stackUrlKeys, } from "@/lib/stack-url-state"; -const getValidIds = (category: keyof typeof TECH_OPTIONS): string[] => { - return TECH_OPTIONS[category]?.map((opt) => opt.id) ?? []; -}; - const CATEGORY_ORDER: Array = [ "webFrontend", "nativeFrontend", @@ -40,207 +36,142 @@ const CATEGORY_ORDER: Array = [ "install", ]; -function getStackKeyFromUrlKey(urlKey: string): keyof StackState | null { - for (const [stackKey, urlKeyValue] of Object.entries(stackUrlKeys)) { - if (urlKeyValue === urlKey) { - return stackKey as keyof StackState; - } - } - return null; -} +const getStackKeyFromUrlKey = (urlKey: string): keyof StackState | null => + (Object.entries(stackUrlKeys).find( + ([, value]) => value === urlKey, + )?.[0] as keyof StackState) || null; -export function parseSearchParamsToStack(searchParams: { - [key: string]: string | string[] | undefined; -}): StackState { +const isDefaultStack = (stack: StackState): boolean => + Object.entries(DEFAULT_STACK).every( + ([key, _defaultValue]) => + key === "projectName" || + isStackDefault( + stack, + key as keyof StackState, + stack[key as keyof StackState], + ), + ); + +export function parseSearchParamsToStack( + searchParams: Record, +): StackState { const parsedStack: StackState = { ...DEFAULT_STACK }; - for (const [key, value] of Object.entries(searchParams)) { - if ( - key === "utm_source" || - key === "utm_medium" || - key === "utm_campaign" - ) { - continue; - } - - const stackKey = getStackKeyFromUrlKey(key); - if (stackKey && value !== undefined) { - try { - const parser = stackParsers[stackKey]; - if (parser) { - const parsedValue = parser.parseServerSide( - Array.isArray(value) ? value[0] : value, - ); - (parsedStack as Record)[stackKey] = parsedValue; + Object.entries(searchParams) + .filter(([key]) => !key.startsWith("utm_")) + .forEach(([key, value]) => { + const stackKey = getStackKeyFromUrlKey(key); + if (stackKey && value !== undefined) { + try { + const parser = stackParsers[stackKey]; + if (parser) { + parsedStack[stackKey] = parser.parseServerSide( + Array.isArray(value) ? value[0] : value, + ) as never; + } + } catch (error) { + console.warn(`Failed to parse ${key}:`, error); } - } catch (error) { - console.warn(`Failed to parse ${key}:`, error); } - } - } - - for (const [key, defaultValue] of Object.entries(DEFAULT_STACK)) { - if (parsedStack[key as keyof StackState] === undefined) { - (parsedStack as Record)[key] = defaultValue; - } - } + }); return parsedStack; } -/** - * Generate a human-readable summary of the stack - */ export function generateStackSummary(stack: StackState): string { - const selectedTechs: string[] = []; + const selectedTechs = CATEGORY_ORDER.flatMap((category) => { + const options = TECH_OPTIONS[category]; + const selectedValue = stack[category as keyof StackState]; - for (const category of CATEGORY_ORDER) { - const categoryKey = category as keyof StackState; - const options = TECH_OPTIONS[category as keyof typeof TECH_OPTIONS]; - const selectedValue = stack[categoryKey]; + if (!options) return []; - if (!options) continue; + const getTechNames = (value: string | string[]) => { + const values = Array.isArray(value) ? value : [value]; + return values + .filter( + (id) => + id !== "none" && + id !== "false" && + !(["git", "install", "auth"].includes(category) && id === "true"), + ) + .map((id) => options.find((opt) => opt.id === id)?.name) + .filter(Boolean) as string[]; + }; - if (Array.isArray(selectedValue)) { - if ( - selectedValue.length === 0 || - (selectedValue.length === 1 && selectedValue[0] === "none") - ) { - continue; - } - - for (const id of selectedValue) { - if (id === "none") continue; - const tech = options.find((opt) => opt.id === id); - if (tech) { - selectedTechs.push(tech.name); - } - } - } else { - const tech = options.find((opt) => opt.id === selectedValue); - if ( - !tech || - tech.id === "none" || - tech.id === "false" || - ((category === "git" || - category === "install" || - category === "auth") && - tech.id === "true") - ) { - continue; - } - selectedTechs.push(tech.name); - } - } + return getTechNames(selectedValue); + }); return selectedTechs.length > 0 ? selectedTechs.join(" • ") : "Custom stack"; } export function generateStackCommand(stack: StackState): string { - let base: string; - switch (stack.packageManager) { - case "npm": - base = "npx create-better-t-stack@latest"; - break; - case "pnpm": - base = "pnpm create better-t-stack@latest"; - break; - default: - base = "bun create better-t-stack@latest"; - break; - } + const packageManagerCommands = { + npm: "npx create-better-t-stack@latest", + pnpm: "pnpm create better-t-stack@latest", + default: "bun create better-t-stack@latest", + }; + const base = + packageManagerCommands[ + stack.packageManager as keyof typeof packageManagerCommands + ] || packageManagerCommands.default; const projectName = stack.projectName || "my-better-t-app"; - const flags: string[] = []; - const isDefaultStack = Object.keys(DEFAULT_STACK).every((key) => { - if (key === "projectName") return true; - const defaultKey = key as keyof StackState; - return isStackDefault(stack, defaultKey, stack[defaultKey]); - }); - - if (isDefaultStack) { - flags.push("--yes"); - } else { - const combinedFrontends = [ - ...stack.webFrontend, - ...stack.nativeFrontend, - ].filter((v, _, arr) => v !== "none" || arr.length === 1); - - if (combinedFrontends.length === 0 || combinedFrontends[0] === "none") { - flags.push("--frontend none"); - } else { - flags.push(`--frontend ${combinedFrontends.join(" ")}`); - } - - flags.push(`--backend ${stack.backend}`); - flags.push(`--runtime ${stack.runtime}`); - flags.push(`--api ${stack.api}`); - flags.push(`--auth ${stack.auth}`); - flags.push(`--database ${stack.database}`); - flags.push(`--orm ${stack.orm}`); - flags.push(`--db-setup ${stack.dbSetup}`); - flags.push(`--package-manager ${stack.packageManager}`); - - if (stack.git === "false") { - flags.push("--no-git"); - } else { - flags.push("--git"); - } - - flags.push(`--web-deploy ${stack.webDeploy}`); - flags.push(`--server-deploy ${stack.serverDeploy}`); - - if (stack.install === "false") { - flags.push("--no-install"); - } else { - flags.push("--install"); - } - - if (stack.addons.length > 0) { - const validAddons = stack.addons.filter((addon) => - [ - "pwa", - "tauri", - "starlight", - "biome", - "husky", - "turborepo", - "ultracite", - "fumadocs", - "oxlint", - "ruler", - ].includes(addon), - ); - if (validAddons.length > 0) { - flags.push(`--addons ${validAddons.join(" ")}`); - } - } else { - flags.push("--addons none"); - } - - if (stack.examples.length > 0) { - flags.push(`--examples ${stack.examples.join(" ")}`); - } else { - flags.push("--examples none"); - } + if (isDefaultStack(stack)) { + return `${base} ${projectName} --yes`; } - return `${base} ${projectName}${ - flags.length > 0 ? ` ${flags.join(" ")}` : "" - }`; + const flags = [ + `--frontend ${ + [...stack.webFrontend, ...stack.nativeFrontend] + .filter((v, _, arr) => v !== "none" || arr.length === 1) + .join(" ") || "none" + }`, + `--backend ${stack.backend}`, + `--runtime ${stack.runtime}`, + `--api ${stack.api}`, + `--auth ${stack.auth}`, + `--database ${stack.database}`, + `--orm ${stack.orm}`, + `--db-setup ${stack.dbSetup}`, + `--package-manager ${stack.packageManager}`, + stack.git === "false" ? "--no-git" : "--git", + `--web-deploy ${stack.webDeploy}`, + `--server-deploy ${stack.serverDeploy}`, + stack.install === "false" ? "--no-install" : "--install", + `--addons ${ + stack.addons.length > 0 + ? stack.addons + .filter((addon) => + [ + "pwa", + "tauri", + "starlight", + "biome", + "husky", + "turborepo", + "ultracite", + "fumadocs", + "oxlint", + "ruler", + ].includes(addon), + ) + .join(" ") || "none" + : "none" + }`, + `--examples ${stack.examples.join(" ") || "none"}`, + ]; + + return `${base} ${projectName} ${flags.join(" ")}`; } -/** - * Generate stack URL from pathname and search params - */ +// URL generation functions export function generateStackUrl( pathname: string, searchParams: URLSearchParams, ): string { const searchString = searchParams.toString(); - const relativeUrl = `${pathname}${searchString ? `?${searchString}` : ""}`; - return `https://better-t-stack.dev${relativeUrl}`; + return `https://better-t-stack.dev${pathname}${searchString ? `?${searchString}` : ""}`; } export function generateStackUrlFromState( @@ -253,212 +184,148 @@ export function generateStackUrlFromState( ? window.location.origin : "https://better-t-stack.dev"); - const isDefaultStack = Object.keys(DEFAULT_STACK).every((key) => { - if (key === "projectName") return true; - const defaultKey = key as keyof StackState; - return isStackDefault(stack, defaultKey, stack[defaultKey]); - }); - - if (isDefaultStack) { + if (isDefaultStack(stack)) { return `${origin}/stack`; } const stackParams = new URLSearchParams(); - - for (const [stackKey, urlKey] of Object.entries(stackUrlKeys)) { + Object.entries(stackUrlKeys).forEach(([stackKey, urlKey]) => { const value = stack[stackKey as keyof StackState]; if (value !== undefined) { - if (Array.isArray(value)) { - stackParams.set(urlKey, value.join(",")); - } else { - stackParams.set(urlKey, String(value)); - } + stackParams.set( + urlKey, + Array.isArray(value) ? value.join(",") : String(value), + ); } - } + }); return `${origin}/stack?${stackParams.toString()}`; } -export function useStackStateWithAllParams() { +// Primary hook - simplified approach +export function useStackState() { const [stack, setStack] = useQueryStates( stackParsers, stackQueryStatesOptions, ); - const setStackWithAllParams = async ( - newStack: Partial | ((prev: StackState) => Partial), + const updateStack = async ( + updates: Partial | ((prev: StackState) => Partial), ) => { - const updatedStack = - typeof newStack === "function" ? newStack(stack) : newStack; - const finalStack = { ...stack, ...updatedStack }; + const newStack = typeof updates === "function" ? updates(stack) : updates; + const finalStack = { ...stack, ...newStack }; - const isFinalStackDefault = Object.keys(DEFAULT_STACK).every((key) => { - if (key === "projectName") return true; - const defaultKey = key as keyof StackState; - return isStackDefault(finalStack, defaultKey, finalStack[defaultKey]); - }); - - if (isFinalStackDefault) { - await setStack(null); - } else { - await setStack(finalStack); - } + await setStack(isDefaultStack(finalStack) ? null : finalStack); }; - return [stack, setStackWithAllParams] as const; + return [stack, updateStack] as const; } +// Individual state hook - kept for backward compatibility but simplified export function useIndividualStackStates() { - const [projectName, setProjectName] = useQueryState( - "name", - parseAsString.withDefault(DEFAULT_STACK.projectName), - ); + const getValidIds = (category: keyof typeof TECH_OPTIONS) => + TECH_OPTIONS[category]?.map((opt) => opt.id) ?? []; - const [webFrontend, setWebFrontend] = useQueryState( - "fe-w", - parseAsArrayOf(parseAsString).withDefault(DEFAULT_STACK.webFrontend), - ); - - const [nativeFrontend, setNativeFrontend] = useQueryState( - "fe-n", - parseAsArrayOf(parseAsString).withDefault(DEFAULT_STACK.nativeFrontend), - ); - - const [runtime, setRuntime] = useQueryState( - "rt", - parseAsStringEnum(getValidIds("runtime")).withDefault( - DEFAULT_STACK.runtime, + // Individual query states + const queryStates = { + projectName: useQueryState( + "name", + parseAsString.withDefault(DEFAULT_STACK.projectName), ), - ); - - const [backend, setBackend] = useQueryState( - "be", - parseAsStringEnum(getValidIds("backend")).withDefault( - DEFAULT_STACK.backend, + webFrontend: useQueryState( + "fe-w", + parseAsArrayOf(parseAsString).withDefault(DEFAULT_STACK.webFrontend), ), - ); - - const [api, setApi] = useQueryState( - "api", - parseAsStringEnum(getValidIds("api")).withDefault(DEFAULT_STACK.api), - ); - - const [database, setDatabase] = useQueryState( - "db", - parseAsStringEnum(getValidIds("database")).withDefault( - DEFAULT_STACK.database, + nativeFrontend: useQueryState( + "fe-n", + parseAsArrayOf(parseAsString).withDefault(DEFAULT_STACK.nativeFrontend), ), - ); - - const [orm, setOrm] = useQueryState( - "orm", - parseAsStringEnum(getValidIds("orm")).withDefault(DEFAULT_STACK.orm), - ); - - const [dbSetup, setDbSetup] = useQueryState( - "dbs", - parseAsStringEnum(getValidIds("dbSetup")).withDefault( - DEFAULT_STACK.dbSetup, + runtime: useQueryState( + "rt", + parseAsStringEnum(getValidIds("runtime")).withDefault( + DEFAULT_STACK.runtime, + ), ), - ); - - const [auth, setAuth] = useQueryState( - "au", - parseAsStringEnum(getValidIds("auth")).withDefault(DEFAULT_STACK.auth), - ); - - const [packageManager, setPackageManager] = useQueryState( - "pm", - parseAsStringEnum(getValidIds("packageManager")).withDefault( - DEFAULT_STACK.packageManager, + backend: useQueryState( + "be", + parseAsStringEnum(getValidIds("backend")).withDefault( + DEFAULT_STACK.backend, + ), ), - ); - - const [addons, setAddons] = useQueryState( - "add", - parseAsArrayOf(parseAsString).withDefault(DEFAULT_STACK.addons), - ); - - const [examples, setExamples] = useQueryState( - "ex", - parseAsArrayOf(parseAsString).withDefault(DEFAULT_STACK.examples), - ); - - const [git, setGit] = useQueryState( - "git", - parseAsStringEnum(["true", "false"] as const).withDefault( - DEFAULT_STACK.git as "true" | "false", + api: useQueryState( + "api", + parseAsStringEnum(getValidIds("api")).withDefault(DEFAULT_STACK.api), ), - ); - - const [install, setInstall] = useQueryState( - "i", - parseAsStringEnum(["true", "false"] as const).withDefault( - DEFAULT_STACK.install as "true" | "false", + database: useQueryState( + "db", + parseAsStringEnum(getValidIds("database")).withDefault( + DEFAULT_STACK.database, + ), ), - ); - - const [webDeploy, setWebDeploy] = useQueryState( - "wd", - parseAsStringEnum(getValidIds("webDeploy")).withDefault( - DEFAULT_STACK.webDeploy, + orm: useQueryState( + "orm", + parseAsStringEnum(getValidIds("orm")).withDefault(DEFAULT_STACK.orm), ), - ); - - const [serverDeploy, setServerDeploy] = useQueryState( - "sd", - parseAsStringEnum(getValidIds("serverDeploy")).withDefault( - DEFAULT_STACK.serverDeploy, + dbSetup: useQueryState( + "dbs", + parseAsStringEnum(getValidIds("dbSetup")).withDefault( + DEFAULT_STACK.dbSetup, + ), + ), + auth: useQueryState( + "au", + parseAsStringEnum(getValidIds("auth")).withDefault(DEFAULT_STACK.auth), + ), + packageManager: useQueryState( + "pm", + parseAsStringEnum(getValidIds("packageManager")).withDefault( + DEFAULT_STACK.packageManager, + ), + ), + addons: useQueryState( + "add", + parseAsArrayOf(parseAsString).withDefault(DEFAULT_STACK.addons), + ), + examples: useQueryState( + "ex", + parseAsArrayOf(parseAsString).withDefault(DEFAULT_STACK.examples), + ), + git: useQueryState( + "git", + parseAsStringEnum(["true", "false"] as const).withDefault( + DEFAULT_STACK.git as "true" | "false", + ), + ), + install: useQueryState( + "i", + parseAsStringEnum(["true", "false"] as const).withDefault( + DEFAULT_STACK.install as "true" | "false", + ), + ), + webDeploy: useQueryState( + "wd", + parseAsStringEnum(getValidIds("webDeploy")).withDefault( + DEFAULT_STACK.webDeploy, + ), + ), + serverDeploy: useQueryState( + "sd", + parseAsStringEnum(getValidIds("serverDeploy")).withDefault( + DEFAULT_STACK.serverDeploy, + ), ), - ); - - const stack: StackState = { - projectName, - webFrontend, - nativeFrontend, - runtime, - backend, - api, - database, - orm, - dbSetup, - auth, - packageManager, - addons, - examples, - git, - install, - webDeploy, - serverDeploy, }; + const stack: StackState = Object.fromEntries( + Object.entries(queryStates).map(([key, [value]]) => [key, value]), + ) as StackState; + const setStack = async (updates: Partial) => { - const setters = { - projectName: setProjectName, - webFrontend: setWebFrontend, - nativeFrontend: setNativeFrontend, - runtime: setRuntime, - backend: setBackend, - api: setApi, - database: setDatabase, - orm: setOrm, - dbSetup: setDbSetup, - auth: setAuth, - packageManager: setPackageManager, - addons: setAddons, - examples: setExamples, - git: setGit, - install: setInstall, - webDeploy: setWebDeploy, - serverDeploy: setServerDeploy, - }; - const promises = Object.entries(updates).map(([key, value]) => { - const setter = setters[key as keyof typeof setters]; - return setter(value as never); + const setter = queryStates[key as keyof typeof queryStates]?.[1]; + return setter?.(value as never); }); - - await Promise.all(promises); + await Promise.all(promises.filter(Boolean)); }; return [stack, setStack] as const;