add solid in stack builder

This commit is contained in:
Aman Varshney
2025-05-05 14:35:44 +05:30
parent 4f89b8bc15
commit 5b7162b98d
3 changed files with 244 additions and 195 deletions

View File

@@ -0,0 +1 @@
<svg viewBox="0 0 256 239" width="256" height="239" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid"><defs><linearGradient x1="-5.859%" y1="38.27%" x2="91.406%" y2="60.924%" id="a"><stop stop-color="#76B3E1" offset="10%"/><stop stop-color="#DCF2FD" offset="30%"/><stop stop-color="#76B3E1" offset="100%"/></linearGradient><linearGradient x1="56.996%" y1="38.44%" x2="37.941%" y2="68.375%" id="b"><stop stop-color="#76B3E1" offset="0%"/><stop stop-color="#4377BB" offset="50%"/><stop stop-color="#1F3B77" offset="100%"/></linearGradient><linearGradient x1="10.709%" y1="34.532%" x2="104.337%" y2="70.454%" id="c"><stop stop-color="#315AA9" offset="0%"/><stop stop-color="#518AC8" offset="50%"/><stop stop-color="#315AA9" offset="100%"/></linearGradient><linearGradient x1="61.993%" y1="29.58%" x2="17.762%" y2="105.119%" id="d"><stop stop-color="#4377BB" offset="0%"/><stop stop-color="#1A336B" offset="50%"/><stop stop-color="#1A336B" offset="100%"/></linearGradient></defs><path d="M256 50.473S170.667-12.32 104.654 2.17l-4.83 1.61c-9.66 3.22-17.71 8.05-22.541 14.49l-3.22 4.83-24.151 41.862 41.862 8.05c17.71 11.271 40.251 16.101 61.182 11.271l74.063 14.49L256 50.474Z" fill="#76B3E1"/><path d="M256 50.473S170.667-12.32 104.654 2.17l-4.83 1.61c-9.66 3.22-17.71 8.05-22.541 14.49l-3.22 4.83-24.151 41.862 41.862 8.05c17.71 11.271 40.251 16.101 61.182 11.271l74.063 14.49L256 50.474Z" fill="url(#a)" opacity=".3"/><path d="m77.283 50.473-6.44 1.61c-27.371 8.05-35.422 33.811-20.931 56.352 16.1 20.931 49.912 32.201 77.283 24.151l99.824-33.811S141.686 35.982 77.283 50.473Z" fill="#518AC8"/><path d="m77.283 50.473-6.44 1.61c-27.371 8.05-35.422 33.811-20.931 56.352 16.1 20.931 49.912 32.201 77.283 24.151l99.824-33.811S141.686 35.982 77.283 50.473Z" fill="url(#b)" opacity=".3"/><path d="M209.308 122.926c-18.44-23.037-49.007-32.59-77.283-24.151l-99.824 32.201L0 187.328l180.327 30.591 32.201-57.962c6.44-11.27 4.83-24.15-3.22-37.031Z" fill="url(#c)"/><path d="M177.107 179.278c-18.44-23.037-49.008-32.59-77.283-24.151L0 187.328s85.333 64.403 151.346 48.302l4.83-1.61c27.371-8.05 37.032-33.811 20.93-54.742Z" fill="url(#d)"/></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -13,6 +13,7 @@ import {
PRESET_TEMPLATES, PRESET_TEMPLATES,
type StackState, type StackState,
TECH_OPTIONS, TECH_OPTIONS,
isStackDefault,
} from "@/lib/constant"; } from "@/lib/constant";
import { stackParsers, stackQueryStatesOptions } from "@/lib/stack-url-state"; import { stackParsers, stackQueryStatesOptions } from "@/lib/stack-url-state";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@@ -87,17 +88,20 @@ const hasWebFrontend = (frontend: string[]) =>
"next", "next",
"nuxt", "nuxt",
"svelte", "svelte",
"solid",
].includes(f), ].includes(f),
); );
const hasNativeFrontend = (frontend: string[]) => frontend.includes("native"); const hasNativeFrontend = (frontend: string[]) => frontend.includes("native");
const hasPWACompatibleFrontend = (frontend: string[]) => 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[]) => const hasTauriCompatibleFrontend = (frontend: string[]) =>
frontend.some((f) => 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 => { 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 { } else {
if (nextStack.runtime === "none") { if (nextStack.runtime === "none") {
notes.runtime.notes.push( notes.runtime.notes.push(
@@ -310,18 +342,18 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
} else { } else {
if (nextStack.orm === "mongoose") { if (nextStack.orm === "mongoose") {
notes.database.notes.push( 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( 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.database.hasIssue = true;
notes.orm.hasIssue = true; notes.orm.hasIssue = true;
nextStack.orm = "prisma"; nextStack.orm = "drizzle";
changed = true; changed = true;
changes.push({ changes.push({
category: "database", 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") { if (nextStack.dbSetup === "mongodb-atlas") {
@@ -457,8 +489,9 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
const isNuxt = nextStack.frontend.includes("nuxt"); const isNuxt = nextStack.frontend.includes("nuxt");
const isSvelte = nextStack.frontend.includes("svelte"); const isSvelte = nextStack.frontend.includes("svelte");
if ((isNuxt || isSvelte) && nextStack.api === "trpc") { const isSolid = nextStack.frontend.includes("solid");
const frontendName = isNuxt ? "Nuxt" : "Svelte"; if ((isNuxt || isSvelte || isSolid) && nextStack.api === "trpc") {
const frontendName = isNuxt ? "Nuxt" : isSvelte ? "Svelte" : "Solid";
notes.api.notes.push( notes.api.notes.push(
`${frontendName} requires oRPC. It will be selected automatically.`, `${frontendName} requires oRPC. It will be selected automatically.`,
); );
@@ -482,25 +515,25 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
if (!isPWACompat && nextStack.addons.includes("pwa")) { if (!isPWACompat && nextStack.addons.includes("pwa")) {
incompatibleAddons.push("pwa"); incompatibleAddons.push("pwa");
notes.frontend.notes.push( 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( 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.frontend.hasIssue = true;
notes.addons.hasIssue = true; notes.addons.hasIssue = true;
changes.push({ changes.push({
category: "addons", category: "addons",
message: "PWA addon removed (requires TanStack or React Router)", message: "PWA addon removed (requires compatible frontend)",
}); });
} }
if (!isTauriCompat && nextStack.addons.includes("tauri")) { if (!isTauriCompat && nextStack.addons.includes("tauri")) {
incompatibleAddons.push("tauri"); incompatibleAddons.push("tauri");
notes.frontend.notes.push( 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( 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.frontend.hasIssue = true;
notes.addons.hasIssue = true; notes.addons.hasIssue = true;
@@ -529,8 +562,7 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
const incompatibleExamples: string[] = []; const incompatibleExamples: string[] = [];
const isWeb = hasWebFrontend(nextStack.frontend); const isWeb = hasWebFrontend(nextStack.frontend);
const isNativeOnly = const isNativeOnly = hasNativeFrontend(nextStack.frontend) && !isWeb;
hasNativeFrontend(nextStack.frontend) && !isWeb && !isConvex;
if (isNativeOnly) { if (isNativeOnly) {
if (nextStack.examples.length > 0) { 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.", "Examples are not supported with Native-only frontend. Examples will be removed.",
); );
notes.examples.notes.push( 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.frontend.hasIssue = true;
notes.examples.hasIssue = true; notes.examples.hasIssue = true;
@@ -582,23 +614,31 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
message: "AI example removed (not compatible with Elysia)", 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)]; const uniqueIncompatibleExamples = [...new Set(incompatibleExamples)];
if (uniqueIncompatibleExamples.length > 0) { if (uniqueIncompatibleExamples.length > 0) {
if ( if (!isWeb && !isNativeOnly) {
!isWeb && if (
(uniqueIncompatibleExamples.includes("todo") || uniqueIncompatibleExamples.includes("todo") ||
uniqueIncompatibleExamples.includes("ai")) uniqueIncompatibleExamples.includes("ai")
) { ) {
notes.frontend.notes.push( notes.frontend.notes.push(
"Examples require a web frontend. Incompatible examples will be removed.", "Examples require a web frontend. Incompatible examples will be removed.",
); );
notes.examples.notes.push( notes.examples.notes.push(
"Requires a web frontend. Incompatible examples will be removed.", "Requires a web frontend. Incompatible examples will be removed.",
); );
notes.frontend.hasIssue = true; notes.frontend.hasIssue = true;
notes.examples.hasIssue = true; notes.examples.hasIssue = true;
}
} }
if ( if (
nextStack.database === "none" && nextStack.database === "none" &&
@@ -626,6 +666,16 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
notes.backend.hasIssue = true; notes.backend.hasIssue = true;
notes.examples.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; const originalExamplesLength = nextStack.examples.length;
nextStack.examples = nextStack.examples.filter( nextStack.examples = nextStack.examples.filter(
@@ -647,6 +697,9 @@ const getCompatibilityRules = (stack: StackState) => {
const hasWebFrontendSelected = hasWebFrontend(stack.frontend); const hasWebFrontendSelected = hasWebFrontend(stack.frontend);
const hasNativeOnly = const hasNativeOnly =
hasNativeFrontend(stack.frontend) && !hasWebFrontendSelected; hasNativeFrontend(stack.frontend) && !hasWebFrontendSelected;
const hasSolid = stack.frontend.includes("solid");
const hasNuxt = stack.frontend.includes("nuxt");
const hasSvelte = stack.frontend.includes("svelte");
return { return {
isConvex, isConvex,
@@ -655,8 +708,10 @@ const getCompatibilityRules = (stack: StackState) => {
hasNativeOnly, hasNativeOnly,
hasPWACompatible: hasPWACompatibleFrontend(stack.frontend), hasPWACompatible: hasPWACompatibleFrontend(stack.frontend),
hasTauriCompatible: hasTauriCompatibleFrontend(stack.frontend), hasTauriCompatible: hasTauriCompatibleFrontend(stack.frontend),
hasNuxtOrSvelte: hasNuxtOrSvelteOrSolid: hasNuxt || hasSvelte || hasSolid,
stack.frontend.includes("nuxt") || stack.frontend.includes("svelte"), hasSolid,
hasNuxt,
hasSvelte,
}; };
}; };
@@ -675,42 +730,14 @@ const generateCommand = (stackState: StackState): string => {
} }
const projectName = stackState.projectName || "my-better-t-app"; const projectName = stackState.projectName || "my-better-t-app";
const flags: string[] = ["--yes"]; const flags: string[] = [];
const isDefault = <K extends keyof StackState>( const checkDefault = <K extends keyof StackState>(
key: K, key: K,
value: StackState[K], value: StackState[K],
) => { ) => isStackDefault(stackState, key, value);
const defaultValue = DEFAULT_STACK[key];
if (stackState.backend === "convex") { if (!checkDefault("frontend", stackState.frontend)) {
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 (stackState.frontend.length === 0 || stackState.frontend[0] === "none") { if (stackState.frontend.length === 0 || stackState.frontend[0] === "none") {
flags.push("--frontend none"); flags.push("--frontend none");
} else { } 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}`); flags.push(`--backend ${stackState.backend}`);
} }
if (stackState.backend !== "convex") { if (stackState.backend !== "convex") {
if (!isDefault("runtime", stackState.runtime)) { if (!checkDefault("runtime", stackState.runtime)) {
flags.push(`--runtime ${stackState.runtime}`); flags.push(`--runtime ${stackState.runtime}`);
} }
if (!isDefault("api", stackState.api)) { if (!checkDefault("api", stackState.api)) {
flags.push(`--api ${stackState.api}`); flags.push(`--api ${stackState.api}`);
} }
if (!isDefault("database", stackState.database)) { if (!checkDefault("database", stackState.database)) {
flags.push(`--database ${stackState.database}`); flags.push(`--database ${stackState.database}`);
} }
if (!isDefault("orm", stackState.orm)) { if (!checkDefault("orm", stackState.orm)) {
flags.push(`--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") { if (stackState.auth === "false" && DEFAULT_STACK.auth === "true") {
flags.push("--no-auth"); flags.push("--no-auth");
} }
} }
if (!isDefault("dbSetup", stackState.dbSetup)) { if (!checkDefault("dbSetup", stackState.dbSetup)) {
flags.push(`--db-setup ${stackState.dbSetup}`); flags.push(`--db-setup ${stackState.dbSetup}`);
} }
} else { } 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}`); flags.push(`--package-manager ${stackState.packageManager}`);
} }
if (!isDefault("git", stackState.git)) { if (!checkDefault("git", stackState.git)) {
if (stackState.git === "false") flags.push("--no-git"); if (stackState.git === "false" && DEFAULT_STACK.git === "true") {
flags.push("--no-git");
}
} }
if (!isDefault("install", stackState.install)) { if (!checkDefault("install", stackState.install)) {
if (stackState.install === "false") flags.push("--no-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) { if (stackState.addons.length > 0) {
flags.push(`--addons ${stackState.addons.join(" ")}`); flags.push(`--addons ${stackState.addons.join(" ")}`);
} else { } 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) { if (stackState.examples.length > 0) {
flags.push(`--examples ${stackState.examples.join(" ")}`); flags.push(`--examples ${stackState.examples.join(" ")}`);
} else { } else {
@@ -782,10 +809,6 @@ const generateCommand = (stackState: StackState): string => {
} }
} }
if (flags.length === 1 && flags[0] === "--yes") {
flags.pop();
}
return `${base} ${projectName}${ return `${base} ${projectName}${
flags.length > 0 ? ` ${flags.join(" ")}` : "" flags.length > 0 ? ` ${flags.join(" ")}` : ""
}`; }`;
@@ -842,7 +865,7 @@ const StackArchitect = () => {
catKey, catKey,
) )
) { ) {
const convexDefaults: Record<string, string> = { const convexDefaults: Record<string, string | string[]> = {
runtime: "none", runtime: "none",
database: "none", database: "none",
orm: "none", orm: "none",
@@ -860,7 +883,6 @@ const StackArchitect = () => {
); );
} }
} }
if (catKey === "examples" && techId !== "todo") { if (catKey === "examples" && techId !== "todo") {
addRule( addRule(
category, category,
@@ -868,6 +890,16 @@ const StackArchitect = () => {
"Convex backend only supports the 'Todo' example.", "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; continue;
} }
@@ -887,11 +919,12 @@ const StackArchitect = () => {
"API 'None' is only available with the Convex backend.", "API 'None' is only available with the Convex backend.",
); );
} }
if (techId === "trpc" && rules.hasNuxtOrSvelteOrSolid) {
if (techId === "trpc" && rules.hasNuxtOrSvelte) { const frontendName = rules.hasNuxt
const frontendName = stack.frontend.includes("nuxt")
? "Nuxt" ? "Nuxt"
: "Svelte"; : rules.hasSvelte
? "Svelte"
: "Solid";
addRule( addRule(
category, category,
techId, techId,
@@ -908,7 +941,6 @@ const StackArchitect = () => {
"Select a database to enable ORM options.", "Select a database to enable ORM options.",
); );
} }
if ( if (
stack.database === "mongodb" && stack.database === "mongodb" &&
techId !== "prisma" && techId !== "prisma" &&
@@ -921,36 +953,31 @@ const StackArchitect = () => {
"MongoDB requires the Prisma or Mongoose ORM.", "MongoDB requires the Prisma or Mongoose ORM.",
); );
} }
if ( if (
stack.dbSetup === "turso" && stack.database !== "mongodb" &&
techId !== "drizzle" && stack.database !== "none" &&
techId !== "none" techId === "mongoose"
) { ) {
addRule( addRule(
category, category,
techId, techId,
"Turso DB setup requires the Drizzle ORM.", "Mongoose ORM is only compatible with MongoDB.",
); );
} }
if (stack.dbSetup === "turso" && techId !== "drizzle") {
if ( addRule(category, techId, "Turso DB setup requires Drizzle ORM.");
stack.dbSetup === "prisma-postgres" && }
techId !== "prisma" && if (stack.dbSetup === "prisma-postgres" && techId !== "prisma") {
techId !== "none"
) {
addRule( addRule(
category, category,
techId, techId,
"Prisma PostgreSQL setup requires Prisma ORM.", "Prisma PostgreSQL setup requires Prisma ORM.",
); );
} }
if ( if (
stack.dbSetup === "mongodb-atlas" && stack.dbSetup === "mongodb-atlas" &&
techId !== "prisma" && techId !== "prisma" &&
techId !== "mongoose" && techId !== "mongoose"
techId !== "none"
) { ) {
addRule( addRule(
category, category,
@@ -958,7 +985,6 @@ const StackArchitect = () => {
"MongoDB Atlas setup requires Prisma or Mongoose ORM.", "MongoDB Atlas setup requires Prisma or Mongoose ORM.",
); );
} }
if (techId === "none") { if (techId === "none") {
if (stack.database === "mongodb") { if (stack.database === "mongodb") {
addRule( addRule(
@@ -973,14 +999,13 @@ const StackArchitect = () => {
if (stack.dbSetup === "prisma-postgres") { if (stack.dbSetup === "prisma-postgres") {
addRule(category, techId, "This DB setup requires Prisma ORM."); addRule(category, techId, "This DB setup requires Prisma ORM.");
} }
} if (stack.dbSetup === "mongodb-atlas") {
addRule(
if (techId === "mongoose" && stack.database !== "mongodb") { category,
addRule( techId,
category, "This DB setup requires Prisma or Mongoose ORM.",
techId, );
"Mongoose ORM is not compatible with relational databases.", }
);
} }
} }
@@ -991,36 +1016,32 @@ const StackArchitect = () => {
techId, techId,
"Select a database before choosing a cloud setup.", "Select a database before choosing a cloud setup.",
); );
} } else {
if (techId === "turso") {
if (techId === "turso") { if (stack.database !== "sqlite") {
if (stack.database !== "sqlite" && stack.database !== "none") { addRule(category, techId, "Turso requires SQLite database.");
addRule(category, techId, "Turso requires SQLite database."); }
} if (stack.orm !== "drizzle") {
if (stack.orm !== "drizzle" && stack.orm !== "none") { addRule(category, techId, "Turso requires Drizzle ORM.");
addRule(category, techId, "Turso requires Drizzle ORM."); }
} } else if (techId === "prisma-postgres") {
} else if (techId === "prisma-postgres") { if (stack.database !== "postgres") {
if (stack.database !== "postgres" && stack.database !== "none") { addRule(category, techId, "Requires PostgreSQL database.");
addRule(category, techId, "Requires PostgreSQL database."); }
} if (stack.orm !== "prisma") {
if (stack.orm !== "prisma" && stack.orm !== "none") { addRule(category, techId, "Requires Prisma ORM.");
addRule(category, techId, "Requires Prisma ORM."); }
} } else if (techId === "mongodb-atlas") {
} else if (techId === "mongodb-atlas") { if (stack.database !== "mongodb") {
if (stack.database !== "mongodb" && stack.database !== "none") { addRule(category, techId, "Requires MongoDB database.");
addRule(category, techId, "Requires MongoDB database."); }
} if (stack.orm !== "prisma" && stack.orm !== "mongoose") {
if ( addRule(category, techId, "Requires Prisma or Mongoose ORM.");
stack.orm !== "prisma" && }
stack.orm !== "mongoose" && } else if (techId === "neon") {
stack.orm !== "none" if (stack.database !== "postgres") {
) { addRule(category, techId, "Requires PostgreSQL database.");
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.");
} }
} }
} }
@@ -1038,15 +1059,14 @@ const StackArchitect = () => {
addRule( addRule(
category, category,
techId, techId,
"Requires TanStack Router or React Router frontend.", "Requires TanStack Router, React Router or Solid frontend.",
); );
} }
if (techId === "tauri" && !rules.hasTauriCompatible) { if (techId === "tauri" && !rules.hasTauriCompatible) {
addRule( addRule(
category, category,
techId, 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.", "Examples are not supported with Native-only frontend.",
); );
} else { } else {
if ( if (!rules.hasWebFrontend) {
(techId === "todo" || techId === "ai") &&
!rules.hasWebFrontend
) {
addRule( addRule(
category, category,
techId, techId,
"Requires a web frontend (TanStack Router, React Router, etc.).", "Requires a web frontend (TanStack Router, React Router, etc.).",
); );
} }
if (techId === "todo" && stack.database === "none") { if (techId === "todo" && stack.database === "none") {
addRule(category, techId, "Todo example requires a database."); addRule(category, techId, "Todo example requires a database.");
} }
if (techId === "ai") {
if (techId === "ai" && stack.backend === "elysia") { if (stack.backend === "elysia") {
addRule( addRule(
category, category,
techId, techId,
"AI example is not compatible with Elysia backend.", "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: <explanation> // biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
useEffect(() => { useEffect(() => {
if (compatibilityAnalysis.adjustedStack) { if (compatibilityAnalysis.adjustedStack) {
if (compatibilityAnalysis.changes.length > 0) {
}
setLastChanges(compatibilityAnalysis.changes); setLastChanges(compatibilityAnalysis.changes);
setStack(compatibilityAnalysis.adjustedStack); setStack(compatibilityAnalysis.adjustedStack);
} }
@@ -1202,7 +1228,9 @@ const StackArchitect = () => {
catKey === "addons" || catKey === "addons" ||
catKey === "examples" catKey === "examples"
) { ) {
const currentArray = [...(currentValue as string[])]; const currentArray = Array.isArray(currentValue)
? [...currentValue]
: [];
let nextArray = [...currentArray]; let nextArray = [...currentArray];
const isSelected = currentArray.includes(techId); const isSelected = currentArray.includes(techId);
@@ -1214,15 +1242,15 @@ const StackArchitect = () => {
"next", "next",
"nuxt", "nuxt",
"svelte", "svelte",
"solid",
]; ];
if (techId === "none") { if (techId === "none") {
nextArray = ["none"]; nextArray = ["none"];
} else if (isSelected) { } else if (isSelected) {
if (currentArray.length > 1 || currentArray.includes("none")) { if (currentArray.length > 1) {
nextArray = nextArray.filter((id) => id !== techId); nextArray = nextArray.filter((id) => id !== techId);
if (nextArray.length === 0 && !currentArray.includes("none")) { } else {
nextArray = ["none"]; nextArray = ["none"];
}
} }
} else { } else {
nextArray = nextArray.filter((id) => id !== "none"); nextArray = nextArray.filter((id) => id !== "none");
@@ -1583,30 +1611,7 @@ const StackArchitect = () => {
TECH_OPTIONS[categoryKey as keyof typeof TECH_OPTIONS] || []; TECH_OPTIONS[categoryKey as keyof typeof TECH_OPTIONS] || [];
const categoryDisplayName = getCategoryDisplayName(categoryKey); const categoryDisplayName = getCategoryDisplayName(categoryKey);
const filteredOptions = categoryOptions.filter((tech) => { const filteredOptions = categoryOptions.filter(() => {
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;
}
return true; return true;
}); });
@@ -1649,6 +1654,7 @@ const StackArchitect = () => {
{filteredOptions.map((tech) => { {filteredOptions.map((tech) => {
let isSelected = false; let isSelected = false;
const category = categoryKey as keyof StackState; const category = categoryKey as keyof StackState;
const currentValue = stack[category];
if ( if (
category === "addons" || category === "addons" ||
@@ -1656,10 +1662,10 @@ const StackArchitect = () => {
category === "frontend" category === "frontend"
) { ) {
isSelected = ( isSelected = (
(stack[category] as string[]) || [] (currentValue as string[]) || []
).includes(tech.id); ).includes(tech.id);
} else { } else {
isSelected = stack[category] === tech.id; isSelected = currentValue === tech.id;
} }
const disabledReason = disabledReasons.get( const disabledReason = disabledReasons.get(

View File

@@ -66,6 +66,14 @@ export const TECH_OPTIONS = {
color: "from-orange-500 to-orange-700", color: "from-orange-500 to-orange-700",
default: false, 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", id: "native",
name: "React Native", name: "React Native",
@@ -417,7 +425,7 @@ export const PRESET_TEMPLATES = [
name: "Convex + React", name: "Convex + React",
description: "Reactive full-stack app with Convex and TanStack Router", description: "Reactive full-stack app with Convex and TanStack Router",
stack: { stack: {
projectName: "my-convex-app", projectName: "my-better-t-app",
frontend: ["tanstack-router"], frontend: ["tanstack-router"],
backend: "convex", backend: "convex",
runtime: "none", runtime: "none",
@@ -438,7 +446,7 @@ export const PRESET_TEMPLATES = [
name: "Mobile App", name: "Mobile App",
description: "React Native with Expo and SQLite database", description: "React Native with Expo and SQLite database",
stack: { stack: {
projectName: "my-native-app", projectName: "my-better-t-app",
frontend: ["native"], frontend: ["native"],
runtime: "bun", runtime: "bun",
backend: "hono", backend: "hono",
@@ -459,7 +467,7 @@ export const PRESET_TEMPLATES = [
name: "API Only", name: "API Only",
description: "Backend API with Hono and PostgreSQL", description: "Backend API with Hono and PostgreSQL",
stack: { stack: {
projectName: "my-api", projectName: "my-better-t-app",
frontend: ["none"], frontend: ["none"],
runtime: "bun", runtime: "bun",
backend: "hono", backend: "hono",
@@ -480,7 +488,7 @@ export const PRESET_TEMPLATES = [
name: "Full Featured", name: "Full Featured",
description: "Complete setup with web, native, Turso, and addons", description: "Complete setup with web, native, Turso, and addons",
stack: { stack: {
projectName: "my-full-app", projectName: "my-better-t-app",
frontend: ["tanstack-router", "native"], frontend: ["tanstack-router", "native"],
runtime: "bun", runtime: "bun",
backend: "hono", backend: "hono",
@@ -531,3 +539,37 @@ export const DEFAULT_STACK: StackState = {
install: "true", install: "true",
api: "trpc", api: "trpc",
}; };
export const isStackDefault = <K extends keyof StackState>(
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;
};