add seperate rows for native and web frontends

This commit is contained in:
Aman Varshney
2025-05-13 11:27:41 +05:30
parent b38a33115a
commit 837d46c675
3 changed files with 174 additions and 84 deletions

View File

@@ -63,7 +63,8 @@ const validateProjectName = (name: string): string | undefined => {
};
const CATEGORY_ORDER: Array<keyof typeof TECH_OPTIONS> = [
"frontend",
"webFrontend",
"nativeFrontend",
"backend",
"runtime",
"api",
@@ -78,8 +79,8 @@ const CATEGORY_ORDER: Array<keyof typeof TECH_OPTIONS> = [
"install",
];
const hasWebFrontend = (frontend: string[]) =>
frontend.some((f) =>
const hasWebFrontend = (webFrontend: string[]) =>
webFrontend.some((f) =>
[
"tanstack-router",
"react-router",
@@ -91,17 +92,17 @@ const hasWebFrontend = (frontend: string[]) =>
].includes(f),
);
const checkHasNativeFrontend = (frontend: string[]) =>
frontend.includes("native-nativewind") ||
frontend.includes("native-unistyles");
const checkHasNativeFrontend = (nativeFrontend: string[]) =>
nativeFrontend.includes("native-nativewind") ||
nativeFrontend.includes("native-unistyles");
const hasPWACompatibleFrontend = (frontend: string[]) =>
frontend.some((f) =>
const hasPWACompatibleFrontend = (webFrontend: string[]) =>
webFrontend.some((f) =>
["tanstack-router", "react-router", "solid"].includes(f),
);
const hasTauriCompatibleFrontend = (frontend: string[]) =>
frontend.some((f) =>
const hasTauriCompatibleFrontend = (webFrontend: string[]) =>
webFrontend.some((f) =>
[
"tanstack-router",
"react-router",
@@ -114,7 +115,8 @@ const hasTauriCompatibleFrontend = (frontend: string[]) =>
const getBadgeColors = (category: string): string => {
switch (category) {
case "frontend":
case "webFrontend":
case "nativeFrontend":
return "border-blue-300 bg-blue-100 text-blue-800 dark:border-blue-700/30 dark:bg-blue-900/30 dark:text-blue-300";
case "runtime":
return "border-amber-300 bg-amber-100 text-amber-800 dark:border-amber-700/30 dark:bg-amber-900/30 dark:text-amber-300";
@@ -229,33 +231,39 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
}
}
const incompatibleConvexFrontends = ["nuxt", "solid"];
const originalFrontendLength = nextStack.frontend.length;
nextStack.frontend = nextStack.frontend.filter(
const originalWebFrontendLength = nextStack.webFrontend.length;
nextStack.webFrontend = nextStack.webFrontend.filter(
(f) => !incompatibleConvexFrontends.includes(f),
);
if (nextStack.frontend.length !== originalFrontendLength) {
if (nextStack.webFrontend.length !== originalWebFrontendLength) {
changed = true;
notes.frontend.notes.push(
notes.webFrontend.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.webFrontend.hasIssue = true;
notes.backend.hasIssue = true;
changes.push({
category: "convex",
message: "Removed incompatible frontends (Nuxt, Solid)",
message: "Removed incompatible web frontends (Nuxt, Solid)",
});
}
if (nextStack.frontend.length === 0) {
nextStack.frontend = ["tanstack-router"];
if (
nextStack.webFrontend.length === 0 ||
nextStack.webFrontend[0] === "none"
) {
nextStack.webFrontend = ["tanstack-router"];
changed = true;
changes.push({
category: "convex",
message: "Frontend defaulted to TanStack Router",
message: "Web Frontend defaulted to TanStack Router",
});
}
if (nextStack.nativeFrontend[0] === "none") {
} else {
}
} else if (isBackendNone) {
const noneOverrides: Partial<StackState> = {
auth: "false",
@@ -506,19 +514,19 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
}
}
const isNuxt = nextStack.frontend.includes("nuxt");
const isSvelte = nextStack.frontend.includes("svelte");
const isSolid = nextStack.frontend.includes("solid");
const isNuxt = nextStack.webFrontend.includes("nuxt");
const isSvelte = nextStack.webFrontend.includes("svelte");
const isSolid = nextStack.webFrontend.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.`,
);
notes.frontend.notes.push(
notes.webFrontend.notes.push(
`Selected ${frontendName}: API will be set to oRPC.`,
);
notes.api.hasIssue = true;
notes.frontend.hasIssue = true;
notes.webFrontend.hasIssue = true;
nextStack.api = "orpc";
changed = true;
changes.push({
@@ -528,37 +536,37 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
}
const incompatibleAddons: string[] = [];
const isPWACompat = hasPWACompatibleFrontend(nextStack.frontend);
const isTauriCompat = hasTauriCompatibleFrontend(nextStack.frontend);
const isPWACompat = hasPWACompatibleFrontend(nextStack.webFrontend);
const isTauriCompat = hasTauriCompatibleFrontend(nextStack.webFrontend);
if (!isPWACompat && nextStack.addons.includes("pwa")) {
incompatibleAddons.push("pwa");
notes.frontend.notes.push(
notes.webFrontend.notes.push(
"PWA addon requires TanStack/React Router or Solid. Addon will be removed.",
);
notes.addons.notes.push(
"PWA requires TanStack/React Router/Solid. It will be removed.",
);
notes.frontend.hasIssue = true;
notes.webFrontend.hasIssue = true;
notes.addons.hasIssue = true;
changes.push({
category: "addons",
message: "PWA addon removed (requires compatible frontend)",
message: "PWA addon removed (requires compatible web frontend)",
});
}
if (!isTauriCompat && nextStack.addons.includes("tauri")) {
incompatibleAddons.push("tauri");
notes.frontend.notes.push(
notes.webFrontend.notes.push(
"Tauri addon requires TanStack/React Router, Nuxt, Svelte, Solid, or Next.js. Addon will be removed.",
);
notes.addons.notes.push(
"Tauri requires TanStack/React Router/Nuxt/Svelte/Solid/Next.js. It will be removed.",
);
notes.frontend.hasIssue = true;
notes.webFrontend.hasIssue = true;
notes.addons.hasIssue = true;
changes.push({
category: "addons",
message: "Tauri addon removed (requires compatible frontend)",
message: "Tauri addon removed (requires compatible web frontend)",
});
}
@@ -580,18 +588,19 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
}
const incompatibleExamples: string[] = [];
const isWeb = hasWebFrontend(nextStack.frontend);
const isNativeOnly = checkHasNativeFrontend(nextStack.frontend) && !isWeb;
const isWeb = hasWebFrontend(nextStack.webFrontend);
const hasNative = checkHasNativeFrontend(nextStack.nativeFrontend);
const isNativeOnly = hasNative && !isWeb;
if (isNativeOnly) {
if (nextStack.examples.length > 0) {
notes.frontend.notes.push(
notes.webFrontend.notes.push(
"Examples are not supported with Native-only frontend. Examples will be removed.",
);
notes.examples.notes.push(
"Examples require a web frontend. They will be removed.",
);
notes.frontend.hasIssue = true;
notes.webFrontend.hasIssue = true;
notes.examples.hasIssue = true;
incompatibleExamples.push(...nextStack.examples);
changes.push({
@@ -653,13 +662,13 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
uniqueIncompatibleExamples.includes("todo") ||
uniqueIncompatibleExamples.includes("ai")
) {
notes.frontend.notes.push(
notes.webFrontend.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.webFrontend.hasIssue = true;
notes.examples.hasIssue = true;
}
}
@@ -690,13 +699,13 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
notes.examples.hasIssue = true;
}
if (isSolid && uniqueIncompatibleExamples.includes("ai")) {
notes.frontend.notes.push(
notes.webFrontend.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.webFrontend.hasIssue = true;
notes.examples.hasIssue = true;
}
@@ -720,12 +729,12 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
const getCompatibilityRules = (stack: StackState) => {
const isConvex = stack.backend === "convex";
const isBackendNone = stack.backend === "none";
const hasWebFrontendSelected = hasWebFrontend(stack.frontend);
const hasNativeFrontend = checkHasNativeFrontend(stack.frontend);
const hasWebFrontendSelected = hasWebFrontend(stack.webFrontend);
const hasNativeFrontend = checkHasNativeFrontend(stack.nativeFrontend);
const hasNativeOnly = hasNativeFrontend && !hasWebFrontendSelected;
const hasSolid = stack.frontend.includes("solid");
const hasNuxt = stack.frontend.includes("nuxt");
const hasSvelte = stack.frontend.includes("svelte");
const hasSolid = stack.webFrontend.includes("solid");
const hasNuxt = stack.webFrontend.includes("nuxt");
const hasSvelte = stack.webFrontend.includes("svelte");
return {
isConvex,
@@ -733,8 +742,8 @@ const getCompatibilityRules = (stack: StackState) => {
hasWebFrontend: hasWebFrontendSelected,
hasNativeFrontend,
hasNativeOnly,
hasPWACompatible: hasPWACompatibleFrontend(stack.frontend),
hasTauriCompatible: hasTauriCompatibleFrontend(stack.frontend),
hasPWACompatible: hasPWACompatibleFrontend(stack.webFrontend),
hasTauriCompatible: hasTauriCompatibleFrontend(stack.webFrontend),
hasNuxtOrSvelteOrSolid: hasNuxt || hasSvelte || hasSolid,
hasSolid,
hasNuxt,
@@ -764,11 +773,34 @@ const generateCommand = (stackState: StackState): string => {
value: StackState[K],
) => isStackDefault(stackState, key, value);
if (!checkDefault("frontend", stackState.frontend)) {
if (stackState.frontend.length === 0 || stackState.frontend[0] === "none") {
if (!checkDefault("webFrontend", stackState.webFrontend)) {
if (
stackState.webFrontend.length === 0 ||
stackState.webFrontend[0] === "none"
) {
flags.push("--frontend none");
} else {
flags.push(`--frontend ${stackState.frontend.join(" ")}`);
flags.push(`--frontend ${stackState.webFrontend.join(" ")}`);
}
}
if (!checkDefault("nativeFrontend", stackState.nativeFrontend)) {
if (
stackState.nativeFrontend.length > 0 &&
stackState.nativeFrontend[0] !== "none"
) {
if (checkDefault("webFrontend", stackState.webFrontend)) {
flags.push(`--frontend ${stackState.nativeFrontend.join(" ")}`);
} else {
const existingFrontendIndex = flags.findIndex((f) =>
f.startsWith("--frontend "),
);
if (existingFrontendIndex !== -1) {
flags[existingFrontendIndex] += ` ${stackState.nativeFrontend.join(
" ",
)}`;
}
}
}
}
@@ -876,12 +908,15 @@ const StackBuilder = () => {
const catKey = category as keyof StackState;
if (["frontend", "addons", "examples"].includes(catKey)) {
if (
["webFrontend", "nativeFrontend", "addons", "examples"].includes(catKey)
) {
const currentValues: string[] = [];
randomStack[catKey as "frontend" | "addons" | "examples"] =
currentValues;
randomStack[
catKey as "webFrontend" | "nativeFrontend" | "addons" | "examples"
] = currentValues;
if (catKey === "frontend") {
if (catKey === "webFrontend" || catKey === "nativeFrontend") {
const randomIndex = Math.floor(Math.random() * options.length);
const selectedOption = options[randomIndex].id;
currentValues.push(selectedOption);
@@ -986,7 +1021,7 @@ const StackBuilder = () => {
);
}
} else if (
catKey === "frontend" &&
catKey === "webFrontend" &&
(techId === "nuxt" || techId === "solid")
) {
addRule(
@@ -1274,9 +1309,15 @@ const StackBuilder = () => {
if (!options) continue;
if (Array.isArray(selectedValue)) {
if (selectedValue.length === 0 || selectedValue[0] === "none") continue;
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) {
badges.push(
@@ -1376,7 +1417,8 @@ const StackBuilder = () => {
const currentValue = currentStack[catKey];
if (
catKey === "frontend" ||
catKey === "webFrontend" ||
catKey === "nativeFrontend" ||
catKey === "addons" ||
catKey === "examples"
) {
@@ -1386,7 +1428,7 @@ const StackBuilder = () => {
let nextArray = [...currentArray];
const isSelected = currentArray.includes(techId);
if (catKey === "frontend") {
if (catKey === "webFrontend") {
const webTypes = [
"tanstack-router",
"react-router",
@@ -1405,19 +1447,15 @@ const StackBuilder = () => {
nextArray = ["none"];
}
} else {
nextArray = nextArray.filter((id) => id !== "none");
if (webTypes.includes(techId)) {
nextArray = nextArray.filter((id) => !webTypes.includes(id));
} else if (techId.startsWith("native-")) {
nextArray = nextArray.filter((id) => !id.startsWith("native-"));
}
nextArray.push(techId);
nextArray = [techId];
}
if (nextArray.length > 1) {
nextArray = nextArray.filter((id) => id !== "none");
}
if (nextArray.length === 0) {
} else if (catKey === "nativeFrontend") {
if (techId === "none") {
nextArray = ["none"];
} else if (isSelected) {
nextArray = ["none"];
} else {
nextArray = [techId];
}
} else {
if (isSelected) {
@@ -1425,6 +1463,16 @@ const StackBuilder = () => {
} else {
nextArray.push(techId);
}
if (nextArray.length > 1) {
nextArray = nextArray.filter((id) => id !== "none");
}
if (
nextArray.length === 0 &&
(catKey === "addons" || catKey === "examples")
) {
} else if (nextArray.length === 0) {
nextArray = ["none"];
}
}
const uniqueNext = [...new Set(nextArray)].sort();
@@ -1730,7 +1778,8 @@ const StackBuilder = () => {
if (
category === "addons" ||
category === "examples" ||
category === "frontend"
category === "webFrontend" ||
category === "nativeFrontend"
) {
isSelected = (
(currentValue as string[]) || []

View File

@@ -23,7 +23,7 @@ export const TECH_OPTIONS = {
color: "from-gray-400 to-gray-600",
},
],
frontend: [
webFrontend: [
{
id: "tanstack-router",
name: "TanStack Router",
@@ -81,6 +81,16 @@ export const TECH_OPTIONS = {
color: "from-blue-600 to-blue-800",
default: false,
},
{
id: "none",
name: "No Web Frontend",
description: "No web-based frontend",
icon: "⚙️",
color: "from-gray-400 to-gray-600",
default: false,
},
],
nativeFrontend: [
{
id: "native-nativewind",
name: "React Native + NativeWind",
@@ -99,9 +109,9 @@ export const TECH_OPTIONS = {
},
{
id: "none",
name: "No Frontend",
description: "API-only backend",
icon: "⚙️",
name: "No Native Frontend",
description: "No native mobile frontend",
icon: "📱",
color: "from-gray-400 to-gray-600",
default: false,
},
@@ -427,7 +437,8 @@ export const PRESET_TEMPLATES = [
description: "Standard web app with TanStack Router, Bun, Hono and SQLite",
stack: {
projectName: "my-better-t-app",
frontend: ["tanstack-router"],
webFrontend: ["tanstack-router"],
nativeFrontend: ["none"],
runtime: "bun",
backend: "hono",
database: "sqlite",
@@ -448,7 +459,8 @@ export const PRESET_TEMPLATES = [
description: "Reactive full-stack app with Convex and TanStack Router",
stack: {
projectName: "my-better-t-app",
frontend: ["tanstack-router"],
webFrontend: ["tanstack-router"],
nativeFrontend: ["none"],
backend: "convex",
runtime: "none",
database: "none",
@@ -469,7 +481,8 @@ export const PRESET_TEMPLATES = [
description: "React Native with Expo and SQLite database",
stack: {
projectName: "my-better-t-app",
frontend: ["native-nativewind"],
webFrontend: ["none"],
nativeFrontend: ["native-nativewind"],
runtime: "bun",
backend: "hono",
database: "sqlite",
@@ -490,7 +503,8 @@ export const PRESET_TEMPLATES = [
description: "Backend API with Hono and PostgreSQL",
stack: {
projectName: "my-better-t-app",
frontend: ["none"],
webFrontend: ["none"],
nativeFrontend: ["none"],
runtime: "bun",
backend: "hono",
database: "postgres",
@@ -511,7 +525,8 @@ export const PRESET_TEMPLATES = [
description: "Complete setup with web, native, Turso, and addons",
stack: {
projectName: "my-better-t-app",
frontend: ["tanstack-router", "native-nativewind"],
webFrontend: ["tanstack-router"],
nativeFrontend: ["native-nativewind"],
runtime: "bun",
backend: "hono",
database: "sqlite",
@@ -530,7 +545,8 @@ export const PRESET_TEMPLATES = [
export type StackState = {
projectName: string;
frontend: string[];
webFrontend: string[];
nativeFrontend: string[];
runtime: string;
backend: string;
database: string;
@@ -547,7 +563,8 @@ export type StackState = {
export const DEFAULT_STACK: StackState = {
projectName: "my-better-t-app",
frontend: ["tanstack-router"],
webFrontend: ["tanstack-router"],
nativeFrontend: ["none"],
runtime: "bun",
backend: "hono",
database: "sqlite",
@@ -585,6 +602,24 @@ export const isStackDefault = <K extends keyof StackState>(
return true;
}
if (key === "webFrontend" && stack.webFrontend) {
const currentWeb = (stack.webFrontend as string[]).filter(
(f) => !f.startsWith("native-") && f !== "none",
);
const currentNative = (stack.webFrontend as string[]).filter((f) =>
f.startsWith("native-"),
);
if (key === "webFrontend") {
const defaultWeb = (DEFAULT_STACK.webFrontend as string[]).sort();
const valueWeb = (value as string[]).sort();
return (
defaultWeb.length === valueWeb.length &&
defaultWeb.every((item, index) => item === valueWeb[index])
);
}
}
if (Array.isArray(defaultValue) && Array.isArray(value)) {
const sortedDefault = [...defaultValue].sort();
const sortedValue = [...value].sort();

View File

@@ -12,7 +12,12 @@ const getValidIds = (category: keyof typeof TECH_OPTIONS): string[] => {
export const stackParsers = {
projectName: parseAsString.withDefault(DEFAULT_STACK.projectName),
frontend: parseAsArrayOf(parseAsString).withDefault(DEFAULT_STACK.frontend),
webFrontend: parseAsArrayOf(parseAsString).withDefault(
DEFAULT_STACK.webFrontend,
),
nativeFrontend: parseAsArrayOf(parseAsString).withDefault(
DEFAULT_STACK.nativeFrontend,
),
runtime: parseAsStringEnum<StackState["runtime"]>(
getValidIds("runtime"),
).withDefault(DEFAULT_STACK.runtime),
@@ -50,7 +55,8 @@ export const stackParsers = {
export const stackUrlKeys: UrlKeys<typeof stackParsers> = {
projectName: "name",
frontend: "fe",
webFrontend: "fe-w",
nativeFrontend: "fe-n",
runtime: "rt",
backend: "be",
api: "api",