fix(web): migrate auth from boolean to multi-option system

This commit is contained in:
Aman Varshney
2025-09-02 01:56:09 +05:30
parent 9802332184
commit ea3d8d9745
8 changed files with 254 additions and 380 deletions

View File

@@ -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),

View File

@@ -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<StackState> = {};
const currentValue = currentStack[catKey];

View File

@@ -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 (
<div className="mx-auto min-h-svh max-w-[1280px]">
@@ -34,7 +34,7 @@ export default function AnalyticsPage({
<MetricsCards
totalProjects={totalProjects}
avgProjectsPerDay={avgProjectsPerDay}
authEnabledPercent={authEnabledPercent}
mostPopularAuth={mostPopularAuth}
mostPopularFrontend={mostPopularFrontend}
mostPopularBackend={mostPopularBackend}
mostPopularORM={mostPopularORM}

View File

@@ -3,7 +3,7 @@ import { Cpu, Download, Terminal, TrendingUp, Users } from "lucide-react";
interface MetricsCardsProps {
totalProjects: number;
avgProjectsPerDay: number;
authEnabledPercent: number;
mostPopularAuth: string;
mostPopularFrontend: string;
mostPopularBackend: string;
mostPopularORM: string;
@@ -14,7 +14,7 @@ interface MetricsCardsProps {
export function MetricsCards({
totalProjects,
avgProjectsPerDay,
authEnabledPercent,
mostPopularAuth,
mostPopularFrontend,
mostPopularBackend,
mostPopularORM,
@@ -117,16 +117,16 @@ export function MetricsCards({
<div className="rounded border border-border">
<div className="border-border border-b px-4 py-3">
<div className="flex items-center justify-between">
<span className="font-semibold text-sm">AUTH_ADOPTION</span>
<span className="font-semibold text-sm">TOP_AUTH</span>
<Users className="h-4 w-4 text-primary" />
</div>
</div>
<div className="p-4">
<div className="font-bold text-2xl text-primary">
{authEnabledPercent}%
<div className="truncate font-bold text-accent text-lg">
{mostPopularAuth}
</div>
<p className="mt-1 text-muted-foreground text-xs">
$ auth_enabled_percentage.sh
$ most_selected_auth.sh
</p>
</div>
</div>

View File

@@ -412,10 +412,12 @@ export function StackConfigurationCharts({
<div className="border-border border-b px-4 py-3">
<div className="flex items-center gap-2">
<span className="text-primary text-xs"></span>
<span className="font-semibold text-sm">AUTH_ADOPTION.PIE</span>
<span className="font-semibold text-sm">
AUTH_DISTRIBUTION.PIE
</span>
</div>
<p className="mt-1 text-muted-foreground text-xs">
Authentication implementation rate
Authentication provider distribution
</p>
</div>
<div className="p-4">
@@ -439,9 +441,11 @@ export function StackConfigurationCharts({
<Cell
key={`auth-${entry.name}`}
fill={
entry.name === "enabled"
entry.name === "better-auth"
? "hsl(var(--chart-1))"
: "hsl(var(--chart-7))"
: entry.name === "clerk"
? "hsl(var(--chart-2))"
: "hsl(var(--chart-3))"
}
/>
))}

View File

@@ -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 = {

View File

@@ -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 (
<Suspense>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
className="grid h-[calc(100vh-64px)] w-full flex-1 grid-cols-1 overflow-hidden"
>
<StackBuilder />
</motion.div>
</Suspense>
<div className="grid h-[calc(100vh-64px)] w-full flex-1 grid-cols-1 overflow-hidden">
<StackBuilder />
</div>
);
}

View File

@@ -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<keyof typeof TECH_OPTIONS> = [
"webFrontend",
"nativeFrontend",
@@ -40,207 +36,142 @@ const CATEGORY_ORDER: Array<keyof typeof TECH_OPTIONS> = [
"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<string, string | string[] | undefined>,
): 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<string, unknown>)[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<string, unknown>)[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<StackState> | ((prev: StackState) => Partial<StackState>),
const updateStack = async (
updates: Partial<StackState> | ((prev: StackState) => Partial<StackState>),
) => {
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<StackState>) => {
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;