Skip to content

fix(web): apply stack compatibility atomically - #1130

Merged
AmanVarshney01 merged 1 commit into
mainfrom
aman/fix-stack-builder-atomic-state
Jul 31, 2026
Merged

fix(web): apply stack compatibility atomically#1130
AmanVarshney01 merged 1 commit into
mainfrom
aman/fix-stack-builder-atomic-state

Conversation

@AmanVarshney01

@AmanVarshney01 AmanVarshney01 commented Jul 31, 2026

Copy link
Copy Markdown
Owner

Summary

  • resolve stack compatibility inside the same functional URL-state update as each user action
  • isolate stack query updates from preview and selected-file state so concurrent controls cannot overwrite each other
  • match CLI affordances for database/ORM, Clerk, Convex Better Auth, and desktop/Docker combinations
  • add deterministic CLI-parity and transition invariants for compatibility changes

Root cause

The follow-up compatibility write introduced in #1129 still ran in a passive effect. The UI rendered the adjusted stack before that second URL write settled, so a rapid follow-up click could be computed from the adjusted UI and then overwritten by the pending compatibility write. Sidebar counts briefly rendered from different snapshots for the same reason.

This change makes selection plus compatibility resolution one atomic functional update and scopes each query-state setter to only the keys it owns.

Verification

  • bun run check
  • cd apps/web && bun run build
  • bun test apps/web/test/stack-builder-compatibility.test.ts apps/web/test/stack-compatibility-invariant.test.ts
  • bun test apps/cli/test/database-orm.test.ts apps/cli/test/database-setup.test.ts apps/cli/test/backend-runtime.test.ts
  • bun test apps/cli/test/deployment.test.ts apps/cli/test/addons.test.ts apps/cli/test/electrobun-addon.test.ts
  • real-browser rapid-click checks on desktop and mobile for Nuxt + Expo, adjusted-badge removal, Preview + stack updates, Workers + Turso, sidebar counts, all web frontend switches, and database-none ORM disabled states

The invariant suite resolves 25,000 deterministic randomized stacks, checks every enabled card across more than 20,000 representative transitions, and validates every result with the CLI compatibility validator.

Summary by CodeRabbit

  • New Features

    • Stack selections now automatically resolve to compatible technology combinations.
    • Compatibility adjustments are applied before saving or sharing stack configurations.
    • Users receive notifications when selections are adjusted for compatibility.
    • Stack progress, generated commands, saved stacks, and sharing links now reflect the effective configuration.
  • Bug Fixes

    • Improved validation for authentication, databases, desktop applications, Docker deployments, and static output combinations.
    • Prevented incompatible technologies from remaining selected or being hidden by conflicting frontend choices.

@vercel

vercel Bot commented Jul 31, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
create-better-t-stack-web Ready Ready Preview Jul 31, 2026 9:29pm

Request Review

@AmanVarshney01
AmanVarshney01 marked this pull request as ready for review July 31, 2026 21:34
@AmanVarshney01
AmanVarshney01 merged commit 54a9c27 into main Jul 31, 2026
3 checks passed
@AmanVarshney01
AmanVarshney01 deleted the aman/fix-stack-builder-atomic-state branch July 31, 2026 21:36
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

Stack compatibility and state resolution

Layer / File(s) Summary
Compatibility rules and option validation
apps/web/src/app/(home)/new/_components/utils.ts
Adds stricter Clerk, Convex Better Auth, desktop addon, Docker deployment, and ORM validation rules.
Iterative stack resolution and updates
apps/web/src/app/(home)/new/_components/stack-builder/use-stack-builder.ts
Adds centralized iterative sanitization, resolved-stack APIs, technology update helpers, toast reporting, and transition-based updates.
Effective stack state and derived outputs
apps/web/src/lib/stack-url-state.client.ts, apps/web/src/app/(home)/new/_components/stack-builder/index.tsx, apps/web/src/app/(home)/new/_components/stack-builder/use-stack-builder.ts
Sanitizes URL stack state and uses the effective compatible stack for rendering, commands, progress, sharing URLs, and saved stacks.
Resolution and transition validation
apps/web/test/stack-builder-compatibility.test.ts, apps/web/test/stack-compatibility-invariant.test.ts
Tests direct updates, compatibility adjustments, invalid selections, Docker restrictions, CLI validation, repeated resolution, and randomized transitions.

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary change: applying stack compatibility updates atomically in the web stack builder.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (9)
apps/web/src/app/(home)/new/_components/utils.ts (2)

743-759: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use display names in these user-facing messages.

Both messages interpolate raw option ids, so users read "tauri and electrobun removed" and "electrobun requires a static React Router export". Nearby messages use display names, for example "Tauri removed (requires compatible frontend)". Map ids to display names for consistent text. The assertions in apps/web/test/stack-builder-compatibility.test.ts at lines 443-445 must be updated with the same change.

Also applies to: 1258-1264


1127-1136: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider aligning the analyzer with this stricter rule.

getDisabledReason now rejects every ORM when no database is selected. analyzeStackCompatibility at lines 507-520 still auto-selects a database when an ORM is set without one. The UI path cannot reach that branch because getTechSelectionUpdate filters incompatible options, but URL state and presets can. Keep the branch as a repair path, or make it match the new rule and clear the ORM instead. Add a short comment that states which behavior is intended.

apps/web/src/app/(home)/new/_components/stack-builder/use-stack-builder.ts (2)

43-71: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Report non-convergence instead of returning silently.

If the loop exhausts MAX_COMPATIBILITY_PASSES, the function returns a stack that analyzeStackCompatibility still wants to change. The caller receives no signal. Add a development-only warning so a future rule cycle is visible outside the invariant test.

♻️ Proposed change
   const finalAnalysis = analyzeStackCompatibility(currentStack);
+  if (process.env.NODE_ENV !== "production" && finalAnalysis.adjustedStack) {
+    console.warn(
+      `Stack compatibility did not converge within ${MAX_COMPATIBILITY_PASSES} passes`,
+      finalAnalysis.changes,
+    );
+  }
   return {
     stack: currentStack,

73-106: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Remove the duplicated resolution inside the update builders.

applyStackUpdate already resolves the current stack before it calls the updater. getSelectedTechRemovalUpdate and getTechSelectionUpdate resolve it again. Each user action therefore runs the compatibility loop three times or more. The result is the same because resolution is idempotent, but the work is duplicated. Document that these builders receive an already-resolved stack, and drop the internal resolveStackCompatibility calls.

apps/web/src/lib/stack-url-state.client.ts (1)

65-87: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Narrow the parameter type to the query-state shape.

Callers pass the full nuqs state, which also holds viewMode and selectedFile. The annotation says StackState. Type the parameter as the inferred nuqs state, for example ReturnType<typeof useQueryStates<typeof stackParsers>>[0] or a named StackQueryState type. The explicit field list then reads as an intentional projection.

apps/web/test/stack-builder-compatibility.test.ts (1)

436-467: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a Tauri counter-case for the exception.

The second test proves the exception holds for Electrobun. The exception in getDockerDesktopConflict also requires that Tauri is absent. Add a case with addons: ["electrobun", "tauri"] on the same Convex + Better Auth + Next.js stack and assert that Docker is rejected. That locks the !selectedDesktopAddons.includes("tauri") condition.

apps/web/test/stack-compatibility-invariant.test.ts (3)

3-13: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Avoid the cross-app relative import into apps/cli/src.

This web test imports CLI internals through ../../cli/src/.... The dependency direction now runs from apps/web into apps/cli private source. packages/types/src/types.ts already exports CLIInput and ProjectConfig, so import the types from the shared package. For validateFullConfig, use the CLI package entry point, or move the shared compatibility rules into a package that both apps consume.


148-165: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Make the ORM comparison order-insensitive.

toEqual on arrays compares order. enabledOrms follows the declaration order of TECH_OPTIONS.orm in apps/web/src/lib/constant.ts. A harmless reorder of those options breaks this test. Sort both sides, or compare sets.

♻️ Proposed change
-      expect(enabledOrms).toEqual(expectedOrmChoices[database]);
+      expect([...enabledOrms].sort()).toEqual([...expectedOrmChoices[database]].sort());

167-215: 🚀 Performance & Scalability | 🔵 Trivial

Make the iteration counts configurable.

RANDOM_STACK_COUNT is 25,000 and the transition test performs more than 20,000 transitions. Each transition resolves compatibility several times and then runs the full CLI validation. The combined work can add a large fixed cost to every CI run and to local test runs. Read both counts from environment variables with the current values as the default, or gate the exhaustive run behind a scheduled job and keep a smaller default for pull requests.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6ef56496-4622-43d5-844b-f62806f65a99

📥 Commits

Reviewing files that changed from the base of the PR and between 489843e and cd44adf.

📒 Files selected for processing (6)
  • apps/web/src/app/(home)/new/_components/stack-builder/index.tsx
  • apps/web/src/app/(home)/new/_components/stack-builder/use-stack-builder.ts
  • apps/web/src/app/(home)/new/_components/utils.ts
  • apps/web/src/lib/stack-url-state.client.ts
  • apps/web/test/stack-builder-compatibility.test.ts
  • apps/web/test/stack-compatibility-invariant.test.ts

Comment on lines +181 to +194
function showCompatibilityChanges(changes: CompatibilityAnalysis["changes"]) {
if (changes.length === 1) {
toast.info(changes[0].message, { duration: 4000 });
return;
}

if (changes.length > 1) {
const message = `${changes.length} compatibility adjustments made:\n${changes
.map((change) => `• ${change.message}`)
.join("\n")}`;

toast.info(message, { duration: 5000 });
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Deduplicate change messages before you build the toast.

resolveStackCompatibility appends the changes of every pass. A rule that fires in two passes contributes the same message twice. The toast then reports an inflated count, for example "3 compatibility adjustments made" with a repeated bullet. Deduplicate by message before you count.

🐛 Proposed fix
-function showCompatibilityChanges(changes: CompatibilityAnalysis["changes"]) {
-  if (changes.length === 1) {
-    toast.info(changes[0].message, { duration: 4000 });
+function showCompatibilityChanges(changes: CompatibilityAnalysis["changes"]) {
+  const messages = [...new Set(changes.map((change) => change.message))];
+
+  if (messages.length === 1) {
+    toast.info(messages[0], { duration: 4000 });
     return;
   }
 
-  if (changes.length > 1) {
-    const message = `${changes.length} compatibility adjustments made:\n${changes
-      .map((change) => `• ${change.message}`)
-      .join("\n")}`;
+  if (messages.length > 1) {
+    const message = `${messages.length} compatibility adjustments made:\n${messages
+      .map((entry) => `• ${entry}`)
+      .join("\n")}`;
 
     toast.info(message, { duration: 5000 });
   }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function showCompatibilityChanges(changes: CompatibilityAnalysis["changes"]) {
if (changes.length === 1) {
toast.info(changes[0].message, { duration: 4000 });
return;
}
if (changes.length > 1) {
const message = `${changes.length} compatibility adjustments made:\n${changes
.map((change) => `• ${change.message}`)
.join("\n")}`;
toast.info(message, { duration: 5000 });
}
}
function showCompatibilityChanges(changes: CompatibilityAnalysis["changes"]) {
const messages = [...new Set(changes.map((change) => change.message))];
if (messages.length === 1) {
toast.info(messages[0], { duration: 4000 });
return;
}
if (messages.length > 1) {
const message = `${messages.length} compatibility adjustments made:\n${messages
.map((entry) => `• ${entry}`)
.join("\n")}`;
toast.info(message, { duration: 5000 });
}
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant