-
-
Notifications
You must be signed in to change notification settings - Fork 253
feat(web): add shareable stack page #556
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
Caution Review failedThe pull request is closed. WalkthroughRemoves backend-based coercions from CLI prompts and adds explicit Convex/none validators; adds comprehensive URL-backed stack utilities and server loader; introduces Stack page, StackDisplay, ShareDialog, QRCode, TechBadge, and stack-builder UX changes; prunes analytics aggregations and adds culori/qrcode deps. Changes
Sequence Diagram(s)sequenceDiagram
participant U as CLI User
participant P as gatherConfig
participant V as validateFullConfig
participant VC as validateConvexConstraints
participant VN as validateBackendNoneConstraints
participant VB as validateBackendConstraints
U->>P: Answer prompts
P-->>U: Config object (no backend coercions)
U->>V: validateFullConfig(config, flags)
V->>VC: if backend === "convex"
VC-->>V: pass / exit on violation
V->>VN: if backend === "none"
VN-->>V: pass / exit on violation
V->>VB: remaining constraints
VB-->>U: success or error exit
sequenceDiagram
participant B as Browser
participant S as StackPage (server)
participant L as loadStackParams
participant D as StackDisplay
participant SB as StackBuilder (client)
participant U as stack-utils
participant Sh as ShareDialog
participant Q as QRCode
B->>S: GET /stack?...
S->>L: parse search params
L-->>S: LoadedStackState
S-->>B: HTML (includes StackDisplay)
B->>D: Render StackDisplay
D->>U: generateStackUrlFromState / generateStackCommand
B->>SB: Interact / edit stack
SB->>U: useStackStateWithAllParams (URL-backed)
SB-->>B: Updated URL & UI
B->>Sh: Open ShareDialog
Sh->>Q: Generate QR (client/server)
Q-->>Sh: Inline SVG
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 💡 Knowledge Base configuration:
You can enable these sources in your CodeRabbit configuration. 📒 Files selected for processing (1)
✨ Finishing Touches
🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 10
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/web/src/components/ai/page-actions.tsx (1)
37-50: ClipboardItem payload type bug: writing a string instead of a Blob.navigator.clipboard.write with ClipboardItem requires Blob/Promise. Current code passes Promise, which will fail in some browsers. Use writeText after fetching (simplest), or wrap content in a Blob.
Apply this diff to use writeText and keep caching:
- try { - await navigator.clipboard.write([ - new ClipboardItem({ - "text/plain": fetch(markdownUrl).then(async (res) => { - const content = await res.text(); - cache.set(markdownUrl, content); - - return content; - }), - }), - ]); - } finally { + try { + const res = await fetch(markdownUrl); + const content = await res.text(); + cache.set(markdownUrl, content); + await navigator.clipboard.writeText(content); + } finally { setLoading(false); }
🧹 Nitpick comments (20)
apps/cli/src/utils/config-validation.ts (1)
390-391: Good call to wire the new validators early in the flow.Order before validateBackendConstraints is correct.
Consider reusing these invariants in validateConfigForProgrammaticUse to avoid divergent behavior between CLI and programmatic usage. I can factor a shared internal helper if you want.
apps/web/src/lib/stack-url-state.ts (1)
82-87: clearOnDefault addition is correct for clean URLs.This will drop default-valued params when using useQueryStates. For parity, ensure per-field useQueryState hooks also pass clearOnDefault (see stack-utils.ts).
apps/web/src/lib/stack-utils.ts (6)
24-41: Avoid CATEGORY_ORDER drift—make it a single source of truth.Define/export CATEGORY_ORDER once (e.g., in constant.ts) and import here and in UI to prevent divergence.
Example (outside this file):
// in constant.ts export const CATEGORY_ORDER = [ "webFrontend","nativeFrontend","backend","runtime","api","database","orm", "dbSetup","webDeploy","serverDeploy","auth","packageManager","addons", "examples","git","install", ] as const;Then here:
import { CATEGORY_ORDER } from "@/lib/constant";
266-279: Shorten share URLs by omitting default-valued params.Skip serializing keys whose values equal DEFAULT_STACK. This matches clearOnDefault behavior elsewhere and keeps links tidy.
- for (const [stackKey, urlKey] of Object.entries(stackUrlKeys)) { - const value = stack[stackKey as keyof StackState]; - if (value !== undefined) { + for (const [stackKey, urlKey] of Object.entries(stackUrlKeys)) { + const key = stackKey as keyof StackState; + const value = stack[key]; + if ( + value !== undefined && + !isStackDefault(stack, key, value as StackState[typeof key]) + ) { if (Array.isArray(value)) { stackParams.set(urlKey, value.join(",")); } else { stackParams.set(urlKey, String(value)); } } }
311-414: Preserve type safety for enum fields in useQueryState.Without generics, parseAsStringEnum widens to string. Mirror stack-url-state generics to keep StackState-accurate types.
- const [runtime, setRuntime] = useQueryState( + const [runtime, setRuntime] = useQueryState( "rt", - parseAsStringEnum(getValidIds("runtime")).withDefault( + parseAsStringEnum<StackState["runtime"]>(getValidIds("runtime")).withDefault( DEFAULT_STACK.runtime, ), ); - const [backend, setBackend] = useQueryState( + const [backend, setBackend] = useQueryState( "be", - parseAsStringEnum(getValidIds("backend")).withDefault( + parseAsStringEnum<StackState["backend"]>(getValidIds("backend")).withDefault( DEFAULT_STACK.backend, ), ); - const [api, setApi] = useQueryState( + const [api, setApi] = useQueryState( "api", - parseAsStringEnum(getValidIds("api")).withDefault(DEFAULT_STACK.api), + parseAsStringEnum<StackState["api"]>(getValidIds("api")).withDefault(DEFAULT_STACK.api), ); - const [database, setDatabase] = useQueryState( + const [database, setDatabase] = useQueryState( "db", - parseAsStringEnum(getValidIds("database")).withDefault( + parseAsStringEnum<StackState["database"]>(getValidIds("database")).withDefault( DEFAULT_STACK.database, ), ); - const [orm, setOrm] = useQueryState( + const [orm, setOrm] = useQueryState( "orm", - parseAsStringEnum(getValidIds("orm")).withDefault(DEFAULT_STACK.orm), + parseAsStringEnum<StackState["orm"]>(getValidIds("orm")).withDefault(DEFAULT_STACK.orm), ); - const [dbSetup, setDbSetup] = useQueryState( + const [dbSetup, setDbSetup] = useQueryState( "dbs", - parseAsStringEnum(getValidIds("dbSetup")).withDefault( + parseAsStringEnum<StackState["dbSetup"]>(getValidIds("dbSetup")).withDefault( DEFAULT_STACK.dbSetup, ), ); - const [auth, setAuth] = useQueryState( + const [auth, setAuth] = useQueryState( "au", - parseAsStringEnum(getValidIds("auth")).withDefault(DEFAULT_STACK.auth), + parseAsStringEnum<StackState["auth"]>(getValidIds("auth")).withDefault(DEFAULT_STACK.auth), ); - const [packageManager, setPackageManager] = useQueryState( + const [packageManager, setPackageManager] = useQueryState( "pm", - parseAsStringEnum(getValidIds("packageManager")).withDefault( + parseAsStringEnum<StackState["packageManager"]>(getValidIds("packageManager")).withDefault( DEFAULT_STACK.packageManager, ), ); - const [webDeploy, setWebDeploy] = useQueryState( + const [webDeploy, setWebDeploy] = useQueryState( "wd", - parseAsStringEnum(getValidIds("webDeploy")).withDefault( + parseAsStringEnum<StackState["webDeploy"]>(getValidIds("webDeploy")).withDefault( DEFAULT_STACK.webDeploy, ), ); - const [serverDeploy, setServerDeploy] = useQueryState( + const [serverDeploy, setServerDeploy] = useQueryState( "sd", - parseAsStringEnum(getValidIds("serverDeploy")).withDefault( + parseAsStringEnum<StackState["serverDeploy"]>(getValidIds("serverDeploy")).withDefault( DEFAULT_STACK.serverDeploy, ), );
311-414: URL hygiene parity for per-field hooks.useQueryState supports options like clearOnDefault/history. For consistent URLs, pass { clearOnDefault: true, history: "replace" } to each hook or factor a common options object.
435-462: Type-safe setter map (avoid casting to never).Build a typed setter record keyed by StackState to remove casts and catch key/value mismatches at compile time.
Example (outside-diff sketch):
const setters: { [K in keyof StackState]: (v: StackState[K]) => Promise<unknown> } = { 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, };
237-245: Consider origin injection for generateStackUrl, like generateStackUrlFromState.Optional: accept baseUrl to avoid hardcoding domain in tests/previews.
apps/web/src/components/ui/kibo-ui/qr-code/server.tsx (3)
20-28: Match client behavior: set margin to 0 (avoids built-in padding).The client QR sets margin: 0; the server QR does not. This causes visual mismatch and layout jitter between SSR and CSR.
Apply:
- const svg = await QR.toString(data, { + const svg = await QR.toString(data, { type: "svg", color: { dark: foreground, light: background, }, width: 200, errorCorrectionLevel: robustness, + margin: 0, });
5-10: Prop parity: server requires colors; client makes them optional.The mismatch between server and client props invites accidental misuse. Either make both optional with sensible defaults or expose distinct names (ServerQRCodeProps/ClientQRCodeProps) to avoid confusion.
Would you like a follow-up PR to extract a shared QR props type with consistent defaults?
35-41: A11y: label the QR container.Add role and aria-label so screen readers can identify the graphic.
return ( <div className={cn("size-full", "[&_svg]:size-full", className)} + role="img" + aria-label="QR code" // biome-ignore lint/security/noDangerouslySetInnerHtml: "Required for SVG" dangerouslySetInnerHTML={{ __html: svg }} {...props} /> );apps/web/src/lib/stack-server.ts (1)
22-61: Type narrowing with StackState[...] generics is ineffective.StackState fields are typed as string, so parseAsStringEnum<StackState["runtime"]> collapses to string and loses narrowing. Not blocking, but misleading.
Either drop the generic arg, or introduce a literal union for each category (e.g., derive from TECH_OPTIONS with stricter types or export TechId unions) and reuse here.
apps/web/src/components/ui/tech-badge.tsx (1)
7-12: Narrow category type to prevent typos and keep styles in sync.Typing category as string hides mistakes and makes getBadgeColors fragile.
-import type { } from "react"; +import type { } from "react"; +import type { TECH_OPTIONS } from "@/lib/constant"; ... -interface TechBadgeProps { +interface TechBadgeProps { icon: string; name: string; - category: string; + category: keyof typeof TECH_OPTIONS; className?: string; } -const getBadgeColors = (category: string): string => { +const getBadgeColors = (category: keyof typeof TECH_OPTIONS): string => {Also applies to: 14-47
apps/web/src/components/ui/kibo-ui/qr-code/index.tsx (1)
44-55: Color parsing robustness.If CSS vars aren’t defined or return non-oklch values at first paint, getOklch falls back—good. Consider memoizing computedStyle reads or defaulting to known hex when regex fails to avoid flicker on theme switch.
I can wire a tiny cache for foreground/background and invalidate on robustness/prop change if you’d like.
apps/web/src/app/(home)/stack/_components/stack-display.tsx (1)
60-67: Prefer a single source of truth for the CLI command.Commands are duplicated constants here and likely elsewhere. Consider moving to stack-utils (e.g., generateStackCommand(selectedPM)) to avoid drift.
I can add a tiny util and tests if helpful.
apps/web/src/components/ui/share-dialog.tsx (2)
43-99: Memoize techBadges to avoid recomputation on every render.This list is derived purely from props; useMemo reduces unnecessary work during re-renders.
- import { useState } from "react"; + import { useMemo, useState } from "react"; @@ - const techBadges = (() => { + const techBadges = useMemo(() => { const badges: React.ReactNode[] = []; for (const category of CATEGORY_ORDER) { // ...unchanged... } return badges; - })(); + }, [stackState]);Also applies to: 12-12
100-109: Clipboard fallback for non-secure contexts.navigator.clipboard fails on http or some browsers. Offer a lightweight textarea fallback inside catch.
const copyToClipboard = async () => { try { await navigator.clipboard.writeText(stackUrl); setCopied(true); toast.success("Link copied to clipboard!"); setTimeout(() => setCopied(false), 2000); } catch { - toast.error("Failed to copy link"); + try { + const ta = document.createElement("textarea"); + ta.value = stackUrl; + ta.setAttribute("readonly", ""); + ta.style.position = "absolute"; + ta.style.left = "-9999px"; + document.body.appendChild(ta); + ta.select(); + document.execCommand("copy"); + document.body.removeChild(ta); + setCopied(true); + toast.success("Link copied to clipboard!"); + setTimeout(() => setCopied(false), 2000); + } catch { + toast.error("Failed to copy link"); + } } };apps/web/src/app/(home)/_components/stack-builder.tsx (3)
1234-1236: Remove unnecessary cast; setStack accepts Partial.Avoid casting a partial to StackState; pass the partial directly.
- startTransition(() => { - setStack(randomStack as StackState); - }); + startTransition(() => { + setStack(randomStack); + });
2085-2098: Avoid double computation of disabled state; compute reason once.You call getDisabledReason twice per option (via isOptionCompatible and again for disabledReason). Compute once to cut work by ~50% for large grids.
- const isDisabled = !isOptionCompatible( - stack, - categoryKey as keyof typeof TECH_OPTIONS, - tech.id, - ); - - const disabledReason = isDisabled - ? getDisabledReason( - stack, - categoryKey as keyof typeof TECH_OPTIONS, - tech.id, - ) - : null; + const disabledReason = getDisabledReason( + stack, + categoryKey as keyof typeof TECH_OPTIONS, + tech.id, + ); + const isDisabled = Boolean(disabledReason);
1562-1562: Prefer structuredClone over JSON stringify/parse for cloning.Keeps types intact for arrays/objects, faster and safer.
- const simulatedStack: StackState = JSON.parse(JSON.stringify(currentStack)); + const simulatedStack: StackState = + typeof structuredClone === "function" + ? structuredClone(currentStack) + : JSON.parse(JSON.stringify(currentStack));
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (16)
apps/cli/src/prompts/config-prompts.ts(0 hunks)apps/cli/src/utils/config-validation.ts(2 hunks)apps/web/lib/cn.ts(0 hunks)apps/web/package.json(3 hunks)apps/web/scripts/generate-analytics.ts(0 hunks)apps/web/src/app/(home)/_components/stack-builder.tsx(19 hunks)apps/web/src/app/(home)/stack/_components/stack-display.tsx(1 hunks)apps/web/src/app/(home)/stack/page.tsx(1 hunks)apps/web/src/components/ai/page-actions.tsx(1 hunks)apps/web/src/components/ui/kibo-ui/qr-code/index.tsx(1 hunks)apps/web/src/components/ui/kibo-ui/qr-code/server.tsx(1 hunks)apps/web/src/components/ui/share-dialog.tsx(1 hunks)apps/web/src/components/ui/tech-badge.tsx(1 hunks)apps/web/src/lib/stack-server.ts(1 hunks)apps/web/src/lib/stack-url-state.ts(1 hunks)apps/web/src/lib/stack-utils.ts(1 hunks)
💤 Files with no reviewable changes (3)
- apps/web/lib/cn.ts
- apps/cli/src/prompts/config-prompts.ts
- apps/web/scripts/generate-analytics.ts
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/convex_rules.mdc)
**/*.{ts,tsx}: Use Id from './_generated/dataModel' to type document ids (e.g., Id<'users'>)
Ensure Record key/value types align with validators (e.g., v.record(v.id('users'), v.string()) => Record<Id<'users'>, string>)
Be strict with types for document ids; prefer Id<'table'> over string
Use 'as const' for string literals in discriminated unions
When using Array and Record types, declare with explicit generic types (e.g., const arr: Array = ...)
Files:
apps/web/src/components/ai/page-actions.tsxapps/web/src/lib/stack-url-state.tsapps/web/src/lib/stack-server.tsapps/web/src/app/(home)/stack/_components/stack-display.tsxapps/web/src/app/(home)/stack/page.tsxapps/web/src/components/ui/tech-badge.tsxapps/web/src/components/ui/kibo-ui/qr-code/index.tsxapps/web/src/components/ui/kibo-ui/qr-code/server.tsxapps/web/src/components/ui/share-dialog.tsxapps/web/src/lib/stack-utils.tsapps/cli/src/utils/config-validation.tsapps/web/src/app/(home)/_components/stack-builder.tsx
**/*.{js,jsx,ts,tsx,mjs,cjs}
📄 CodeRabbit inference engine (.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc)
**/*.{js,jsx,ts,tsx,mjs,cjs}: Do not use dotenv; Bun auto-loads .env
UseBun.serve()for HTTP/WebSockets; do not useexpress
Usebun:sqlitefor SQLite; do not usebetter-sqlite3
UseBun.redisfor Redis; do not useioredis
UseBun.sqlfor Postgres; do not usepgorpostgres.js
Use built-inWebSocket; do not usews
PreferBun.fileovernode:fsreadFile/writeFile
UseBun.$instead ofexecafor shelling out
Files:
apps/web/src/components/ai/page-actions.tsxapps/web/src/lib/stack-url-state.tsapps/web/src/lib/stack-server.tsapps/web/src/app/(home)/stack/_components/stack-display.tsxapps/web/src/app/(home)/stack/page.tsxapps/web/src/components/ui/tech-badge.tsxapps/web/src/components/ui/kibo-ui/qr-code/index.tsxapps/web/src/components/ui/kibo-ui/qr-code/server.tsxapps/web/src/components/ui/share-dialog.tsxapps/web/src/lib/stack-utils.tsapps/cli/src/utils/config-validation.tsapps/web/src/app/(home)/_components/stack-builder.tsx
**/package.json
📄 CodeRabbit inference engine (.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc)
In package.json scripts, prefer running files with
bun <file>instead ofnode <file>orts-node <file>
Files:
apps/web/package.json
{**/package.json,**/@(jest|vitest).config.@(js|ts|mjs|cjs)}
📄 CodeRabbit inference engine (.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc)
{**/package.json,**/@(jest|vitest).config.@(js|ts|mjs|cjs)}: Usebun testinstead ofjestorvitest
Usebun testto run tests
Files:
apps/web/package.json
{**/package.json,**/webpack.config.@(js|ts|mjs|cjs),**/esbuild.config.@(js|ts|mjs|cjs)}
📄 CodeRabbit inference engine (.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc)
Use
bun build <file>instead ofwebpackoresbuild
Files:
apps/web/package.json
{**/package.json,**/vite.config.@(js|ts|mjs|cjs)}
📄 CodeRabbit inference engine (.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc)
Use HTML imports with
Bun.serve(); do not usevite
Files:
apps/web/package.json
🧠 Learnings (2)
📚 Learning: 2025-08-24T18:00:39.152Z
Learnt from: CR
PR: AmanVarshney01/create-better-t-stack#0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-08-24T18:00:39.152Z
Learning: Applies to convex/**/*.ts : Always include argument and return validators for all Convex functions (public and internal)
Applied to files:
apps/cli/src/utils/config-validation.ts
📚 Learning: 2025-08-24T18:00:39.152Z
Learnt from: CR
PR: AmanVarshney01/create-better-t-stack#0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-08-24T18:00:39.152Z
Learning: Applies to convex/**/*.ts : Always use the new Convex function syntax (query/mutation/action/internal*) with args/returns/handler
Applied to files:
apps/cli/src/utils/config-validation.ts
🧬 Code graph analysis (9)
apps/web/src/lib/stack-server.ts (1)
apps/web/src/lib/constant.ts (3)
TECH_OPTIONS(5-582)DEFAULT_STACK(727-745)StackState(707-725)
apps/web/src/app/(home)/stack/_components/stack-display.tsx (4)
apps/web/src/lib/stack-server.ts (1)
LoadedStackState(65-65)apps/web/src/lib/constant.ts (2)
TECH_OPTIONS(5-582)StackState(707-725)apps/web/src/lib/stack-utils.ts (3)
generateStackUrl(237-244)generateStackSummary(94-137)CATEGORY_ORDER(467-467)apps/web/src/components/ui/tech-badge.tsx (1)
TechBadge(94-109)
apps/web/src/app/(home)/stack/page.tsx (2)
apps/web/src/lib/stack-server.ts (1)
loadStackParams(63-63)apps/web/src/app/(home)/stack/_components/stack-display.tsx (1)
StackDisplay(50-332)
apps/web/src/components/ui/kibo-ui/qr-code/index.tsx (1)
apps/web/src/components/ui/kibo-ui/qr-code/server.tsx (2)
QRCodeProps(5-10)QRCode(12-42)
apps/web/src/components/ui/kibo-ui/qr-code/server.tsx (1)
apps/web/src/components/ui/kibo-ui/qr-code/index.tsx (2)
QRCodeProps(8-13)QRCode(31-88)
apps/web/src/components/ui/share-dialog.tsx (5)
apps/web/src/lib/constant.ts (2)
StackState(707-725)TECH_OPTIONS(5-582)apps/web/src/lib/stack-utils.ts (1)
CATEGORY_ORDER(467-467)apps/web/src/components/ui/tech-badge.tsx (1)
TechBadge(94-109)apps/web/src/components/ui/kibo-ui/qr-code/index.tsx (1)
QRCode(31-88)apps/web/src/components/ui/kibo-ui/qr-code/server.tsx (1)
QRCode(12-42)
apps/web/src/lib/stack-utils.ts (2)
apps/web/src/lib/constant.ts (4)
TECH_OPTIONS(5-582)StackState(707-725)DEFAULT_STACK(727-745)isStackDefault(747-796)apps/web/src/lib/stack-url-state.ts (3)
stackUrlKeys(62-80)stackParsers(13-60)stackQueryStatesOptions(82-87)
apps/cli/src/utils/config-validation.ts (2)
apps/cli/src/types.ts (1)
ProjectConfig(164-183)apps/cli/src/utils/errors.ts (1)
exitWithError(9-15)
apps/web/src/app/(home)/_components/stack-builder.tsx (4)
apps/web/src/lib/stack-utils.ts (3)
useStackStateWithAllParams(282-309)generateStackUrlFromState(246-280)generateStackCommand(139-232)apps/web/src/lib/constant.ts (4)
StackState(707-725)DEFAULT_STACK(727-745)TECH_OPTIONS(5-582)PRESET_TEMPLATES(584-705)apps/web/src/components/ui/share-dialog.tsx (1)
ShareDialog(36-235)apps/web/src/components/ui/dropdown-menu.tsx (4)
DropdownMenu(242-242)DropdownMenuTrigger(244-244)DropdownMenuContent(245-245)DropdownMenuItem(248-248)
🪛 ast-grep (0.38.6)
apps/web/src/components/ui/kibo-ui/qr-code/index.tsx
[warning] 83-83: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html
(react-unsafe-html-injection)
apps/web/src/components/ui/kibo-ui/qr-code/server.tsx
[warning] 37-37: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html
(react-unsafe-html-injection)
🔇 Additional comments (8)
apps/web/src/components/ai/page-actions.tsx (1)
18-18: Import alias switch to @/lib/utils looks good—verify path mapping.Ensure tsconfig/jsconfig path aliases resolve "@/lib/utils" the same way across build, test, and tooling.
apps/web/package.json (1)
30-31: New deps OK; verify edge/browser compatibility and bundle size.qrcode and culori look good; confirm they don’t pull in Node-only modules in Cloudflare Workers and that they’re tree-shaken in client bundles.
Run in apps/web:
# Find all imports of qrcode/culori rg -nC2 -P "from ['\"]qrcode['\"]|from ['\"]culori['\"]" src # Inspect Browser/Module entries in the installed packages jq '{browser,module}' node_modules/qrcode/package.json jq '{browser,module}' node_modules/culori/package.json # Verify the Next.js build for any edge/browser errors npm run buildAlso applies to lines 42–43, 57, 61.
apps/web/src/app/(home)/stack/page.tsx (1)
6-8: Confirm loadStackParams signature vs. searchParams Promise.Many Next.js loaders expect a resolved object, not a Promise. If needed, await searchParams first.
-export default async function StackPage({ searchParams }: StackPageProps) { - const stackState = await loadStackParams(searchParams); +export default async function StackPage({ searchParams }: StackPageProps) { + const sp = await searchParams; + const stackState = await loadStackParams(sp);Also applies to: 34-36
apps/web/src/components/ui/kibo-ui/qr-code/server.tsx (1)
35-41: XSS note for dangerouslySetInnerHTML.Acceptable here because the SVG string is generated by the qrcode library and does not embed raw input as HTML. Keep inputs confined to QR data; avoid passing untrusted SVG strings.
If desired, I can add a tiny invariant check that ensures the returned SVG starts with "<svg" and contains no "<script" before injection.
apps/web/src/components/ui/kibo-ui/qr-code/index.tsx (1)
83-87: XSS note for dangerouslySetInnerHTML (client).Same as server: acceptable if the string only comes from qrcode’s generator. Avoid piping arbitrary SVG.
Optionally add a simple guard before setSVG to assert newSvg.startsWith("<svg") and does not contain "<script".
apps/web/src/components/ui/share-dialog.tsx (1)
122-131: LinkedIn sharing API may ignore summary parameter. Verify endpoint.share-offsite generally honors just the url param; adding text often has no effect. Consider using shareArticle or trimming to url-only if summary is ignored.
Would you like me to adjust to the currently supported LinkedIn endpoint after confirming?
apps/web/src/app/(home)/_components/stack-builder.tsx (2)
1679-1685: Revisit “Database requires an ORM first” gating.This prevents selecting a database until an ORM is chosen, which may be counterintuitive vs. selecting DB then guiding ORM. Confirm product intent; otherwise, allow DB first and auto/guide ORM.
1134-1164: Alchemy deploy gating looks consistent with Wrangler rules.Nice addition aligning serverDeploy with runtime/backend constraints and user guidance via notes and change list.
| export function validateConvexConstraints( | ||
| config: Partial<ProjectConfig>, | ||
| providedFlags: Set<string>, | ||
| ): void { | ||
| const { backend } = config; | ||
|
|
||
| if (backend !== "convex") { | ||
| return; | ||
| } | ||
|
|
||
| const has = (k: string) => providedFlags.has(k); | ||
|
|
||
| if (has("runtime") && config.runtime !== "none") { | ||
| exitWithError( | ||
| "Convex backend requires '--runtime none'. Please remove the --runtime flag or set it to 'none'.", | ||
| ); | ||
| } | ||
|
|
||
| if (has("database") && config.database !== "none") { | ||
| exitWithError( | ||
| "Convex backend requires '--database none'. Please remove the --database flag or set it to 'none'.", | ||
| ); | ||
| } | ||
|
|
||
| if (has("orm") && config.orm !== "none") { | ||
| exitWithError( | ||
| "Convex backend requires '--orm none'. Please remove the --orm flag or set it to 'none'.", | ||
| ); | ||
| } | ||
|
|
||
| if (has("api") && config.api !== "none") { | ||
| exitWithError( | ||
| "Convex backend requires '--api none'. Please remove the --api flag or set it to 'none'.", | ||
| ); | ||
| } | ||
|
|
||
| if (has("dbSetup") && config.dbSetup !== "none") { | ||
| exitWithError( | ||
| "Convex backend requires '--db-setup none'. Please remove the --db-setup flag or set it to 'none'.", | ||
| ); | ||
| } | ||
|
|
||
| if (has("serverDeploy") && config.serverDeploy !== "none") { | ||
| exitWithError( | ||
| "Convex backend requires '--server-deploy none'. Please remove the --server-deploy flag or set it to 'none'.", | ||
| ); | ||
| } | ||
|
|
||
| if (has("auth") && config.auth === "better-auth") { | ||
| exitWithError( | ||
| "Better-Auth is not compatible with Convex backend. Please use '--auth clerk' or '--auth none'.", | ||
| ); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: flag-gated checks miss invalid prompt selections (e.g., Convex + Better-Auth).
These constraints only trigger when the corresponding CLI flag was provided. In interactive/prompt flows (no flags), invalid combos like backend=convex + auth=better-auth or runtime!=='none' will slip through. Enforce regardless of flags (guard with value presence to avoid false positives during partial assembly).
Apply this diff:
@@
export function validateConvexConstraints(
config: Partial<ProjectConfig>,
providedFlags: Set<string>,
): void {
const { backend } = config;
if (backend !== "convex") {
return;
}
- const has = (k: string) => providedFlags.has(k);
-
- if (has("runtime") && config.runtime !== "none") {
+ if (config.runtime && config.runtime !== "none") {
exitWithError(
"Convex backend requires '--runtime none'. Please remove the --runtime flag or set it to 'none'.",
);
}
- if (has("database") && config.database !== "none") {
+ if (config.database && config.database !== "none") {
exitWithError(
"Convex backend requires '--database none'. Please remove the --database flag or set it to 'none'.",
);
}
- if (has("orm") && config.orm !== "none") {
+ if (config.orm && config.orm !== "none") {
exitWithError(
"Convex backend requires '--orm none'. Please remove the --orm flag or set it to 'none'.",
);
}
- if (has("api") && config.api !== "none") {
+ if (config.api && config.api !== "none") {
exitWithError(
"Convex backend requires '--api none'. Please remove the --api flag or set it to 'none'.",
);
}
- if (has("dbSetup") && config.dbSetup !== "none") {
+ if (config.dbSetup && config.dbSetup !== "none") {
exitWithError(
"Convex backend requires '--db-setup none'. Please remove the --db-setup flag or set it to 'none'.",
);
}
- if (has("serverDeploy") && config.serverDeploy !== "none") {
+ if (config.serverDeploy && config.serverDeploy !== "none") {
exitWithError(
"Convex backend requires '--server-deploy none'. Please remove the --server-deploy flag or set it to 'none'.",
);
}
- if (has("auth") && config.auth === "better-auth") {
+ if (config.auth === "better-auth") {
exitWithError(
"Better-Auth is not compatible with Convex backend. Please use '--auth clerk' or '--auth none'.",
);
}
}📝 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.
| export function validateConvexConstraints( | |
| config: Partial<ProjectConfig>, | |
| providedFlags: Set<string>, | |
| ): void { | |
| const { backend } = config; | |
| if (backend !== "convex") { | |
| return; | |
| } | |
| const has = (k: string) => providedFlags.has(k); | |
| if (has("runtime") && config.runtime !== "none") { | |
| exitWithError( | |
| "Convex backend requires '--runtime none'. Please remove the --runtime flag or set it to 'none'.", | |
| ); | |
| } | |
| if (has("database") && config.database !== "none") { | |
| exitWithError( | |
| "Convex backend requires '--database none'. Please remove the --database flag or set it to 'none'.", | |
| ); | |
| } | |
| if (has("orm") && config.orm !== "none") { | |
| exitWithError( | |
| "Convex backend requires '--orm none'. Please remove the --orm flag or set it to 'none'.", | |
| ); | |
| } | |
| if (has("api") && config.api !== "none") { | |
| exitWithError( | |
| "Convex backend requires '--api none'. Please remove the --api flag or set it to 'none'.", | |
| ); | |
| } | |
| if (has("dbSetup") && config.dbSetup !== "none") { | |
| exitWithError( | |
| "Convex backend requires '--db-setup none'. Please remove the --db-setup flag or set it to 'none'.", | |
| ); | |
| } | |
| if (has("serverDeploy") && config.serverDeploy !== "none") { | |
| exitWithError( | |
| "Convex backend requires '--server-deploy none'. Please remove the --server-deploy flag or set it to 'none'.", | |
| ); | |
| } | |
| if (has("auth") && config.auth === "better-auth") { | |
| exitWithError( | |
| "Better-Auth is not compatible with Convex backend. Please use '--auth clerk' or '--auth none'.", | |
| ); | |
| } | |
| } | |
| export function validateConvexConstraints( | |
| config: Partial<ProjectConfig>, | |
| providedFlags: Set<string>, | |
| ): void { | |
| const { backend } = config; | |
| if (backend !== "convex") { | |
| return; | |
| } | |
| if (config.runtime && config.runtime !== "none") { | |
| exitWithError( | |
| "Convex backend requires '--runtime none'. Please remove the --runtime flag or set it to 'none'.", | |
| ); | |
| } | |
| if (config.database && config.database !== "none") { | |
| exitWithError( | |
| "Convex backend requires '--database none'. Please remove the --database flag or set it to 'none'.", | |
| ); | |
| } | |
| if (config.orm && config.orm !== "none") { | |
| exitWithError( | |
| "Convex backend requires '--orm none'. Please remove the --orm flag or set it to 'none'.", | |
| ); | |
| } | |
| if (config.api && config.api !== "none") { | |
| exitWithError( | |
| "Convex backend requires '--api none'. Please remove the --api flag or set it to 'none'.", | |
| ); | |
| } | |
| if (config.dbSetup && config.dbSetup !== "none") { | |
| exitWithError( | |
| "Convex backend requires '--db-setup none'. Please remove the --db-setup flag or set it to 'none'.", | |
| ); | |
| } | |
| if (config.serverDeploy && config.serverDeploy !== "none") { | |
| exitWithError( | |
| "Convex backend requires '--server-deploy none'. Please remove the --server-deploy flag or set it to 'none'.", | |
| ); | |
| } | |
| if (config.auth === "better-auth") { | |
| exitWithError( | |
| "Better-Auth is not compatible with Convex backend. Please use '--auth clerk' or '--auth none'.", | |
| ); | |
| } | |
| } |
| export function validateBackendNoneConstraints( | ||
| config: Partial<ProjectConfig>, | ||
| providedFlags: Set<string>, | ||
| ): void { | ||
| const { backend } = config; | ||
|
|
||
| if (backend !== "none") { | ||
| return; | ||
| } | ||
|
|
||
| const has = (k: string) => providedFlags.has(k); | ||
|
|
||
| if (has("runtime") && config.runtime !== "none") { | ||
| exitWithError( | ||
| "Backend 'none' requires '--runtime none'. Please remove the --runtime flag or set it to 'none'.", | ||
| ); | ||
| } | ||
|
|
||
| if (has("database") && config.database !== "none") { | ||
| exitWithError( | ||
| "Backend 'none' requires '--database none'. Please remove the --database flag or set it to 'none'.", | ||
| ); | ||
| } | ||
|
|
||
| if (has("orm") && config.orm !== "none") { | ||
| exitWithError( | ||
| "Backend 'none' requires '--orm none'. Please remove the --orm flag or set it to 'none'.", | ||
| ); | ||
| } | ||
|
|
||
| if (has("api") && config.api !== "none") { | ||
| exitWithError( | ||
| "Backend 'none' requires '--api none'. Please remove the --api flag or set it to 'none'.", | ||
| ); | ||
| } | ||
|
|
||
| if (has("auth") && config.auth !== "none") { | ||
| exitWithError( | ||
| "Backend 'none' requires '--auth none'. Please remove the --auth flag or set it to 'none'.", | ||
| ); | ||
| } | ||
|
|
||
| if (has("dbSetup") && config.dbSetup !== "none") { | ||
| exitWithError( | ||
| "Backend 'none' requires '--db-setup none'. Please remove the --db-setup flag or set it to 'none'.", | ||
| ); | ||
| } | ||
|
|
||
| if (has("serverDeploy") && config.serverDeploy !== "none") { | ||
| exitWithError( | ||
| "Backend 'none' requires '--server-deploy none'. Please remove the --server-deploy flag or set it to 'none'.", | ||
| ); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same issue for backend='none': invalid prompt combos bypass checks.
Flag-gating allows backend='none' with api/auth/orm/etc via prompts. Make these checks value-driven, not flag-driven.
Apply this diff:
@@
export function validateBackendNoneConstraints(
config: Partial<ProjectConfig>,
providedFlags: Set<string>,
): void {
const { backend } = config;
if (backend !== "none") {
return;
}
- const has = (k: string) => providedFlags.has(k);
-
- if (has("runtime") && config.runtime !== "none") {
+ if (config.runtime && config.runtime !== "none") {
exitWithError(
"Backend 'none' requires '--runtime none'. Please remove the --runtime flag or set it to 'none'.",
);
}
- if (has("database") && config.database !== "none") {
+ if (config.database && config.database !== "none") {
exitWithError(
"Backend 'none' requires '--database none'. Please remove the --database flag or set it to 'none'.",
);
}
- if (has("orm") && config.orm !== "none") {
+ if (config.orm && config.orm !== "none") {
exitWithError(
"Backend 'none' requires '--orm none'. Please remove the --orm flag or set it to 'none'.",
);
}
- if (has("api") && config.api !== "none") {
+ if (config.api && config.api !== "none") {
exitWithError(
"Backend 'none' requires '--api none'. Please remove the --api flag or set it to 'none'.",
);
}
- if (has("auth") && config.auth !== "none") {
+ if (config.auth && config.auth !== "none") {
exitWithError(
"Backend 'none' requires '--auth none'. Please remove the --auth flag or set it to 'none'.",
);
}
- if (has("dbSetup") && config.dbSetup !== "none") {
+ if (config.dbSetup && config.dbSetup !== "none") {
exitWithError(
"Backend 'none' requires '--db-setup none'. Please remove the --db-setup flag or set it to 'none'.",
);
}
- if (has("serverDeploy") && config.serverDeploy !== "none") {
+ if (config.serverDeploy && config.serverDeploy !== "none") {
exitWithError(
"Backend 'none' requires '--server-deploy none'. Please remove the --server-deploy flag or set it to 'none'.",
);
}
}📝 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.
| export function validateBackendNoneConstraints( | |
| config: Partial<ProjectConfig>, | |
| providedFlags: Set<string>, | |
| ): void { | |
| const { backend } = config; | |
| if (backend !== "none") { | |
| return; | |
| } | |
| const has = (k: string) => providedFlags.has(k); | |
| if (has("runtime") && config.runtime !== "none") { | |
| exitWithError( | |
| "Backend 'none' requires '--runtime none'. Please remove the --runtime flag or set it to 'none'.", | |
| ); | |
| } | |
| if (has("database") && config.database !== "none") { | |
| exitWithError( | |
| "Backend 'none' requires '--database none'. Please remove the --database flag or set it to 'none'.", | |
| ); | |
| } | |
| if (has("orm") && config.orm !== "none") { | |
| exitWithError( | |
| "Backend 'none' requires '--orm none'. Please remove the --orm flag or set it to 'none'.", | |
| ); | |
| } | |
| if (has("api") && config.api !== "none") { | |
| exitWithError( | |
| "Backend 'none' requires '--api none'. Please remove the --api flag or set it to 'none'.", | |
| ); | |
| } | |
| if (has("auth") && config.auth !== "none") { | |
| exitWithError( | |
| "Backend 'none' requires '--auth none'. Please remove the --auth flag or set it to 'none'.", | |
| ); | |
| } | |
| if (has("dbSetup") && config.dbSetup !== "none") { | |
| exitWithError( | |
| "Backend 'none' requires '--db-setup none'. Please remove the --db-setup flag or set it to 'none'.", | |
| ); | |
| } | |
| if (has("serverDeploy") && config.serverDeploy !== "none") { | |
| exitWithError( | |
| "Backend 'none' requires '--server-deploy none'. Please remove the --server-deploy flag or set it to 'none'.", | |
| ); | |
| } | |
| } | |
| export function validateBackendNoneConstraints( | |
| config: Partial<ProjectConfig>, | |
| providedFlags: Set<string>, | |
| ): void { | |
| const { backend } = config; | |
| if (backend !== "none") { | |
| return; | |
| } | |
| if (config.runtime && config.runtime !== "none") { | |
| exitWithError( | |
| "Backend 'none' requires '--runtime none'. Please remove the --runtime flag or set it to 'none'.", | |
| ); | |
| } | |
| if (config.database && config.database !== "none") { | |
| exitWithError( | |
| "Backend 'none' requires '--database none'. Please remove the --database flag or set it to 'none'.", | |
| ); | |
| } | |
| if (config.orm && config.orm !== "none") { | |
| exitWithError( | |
| "Backend 'none' requires '--orm none'. Please remove the --orm flag or set it to 'none'.", | |
| ); | |
| } | |
| if (config.api && config.api !== "none") { | |
| exitWithError( | |
| "Backend 'none' requires '--api none'. Please remove the --api flag or set it to 'none'.", | |
| ); | |
| } | |
| if (config.auth && config.auth !== "none") { | |
| exitWithError( | |
| "Backend 'none' requires '--auth none'. Please remove the --auth flag or set it to 'none'.", | |
| ); | |
| } | |
| if (config.dbSetup && config.dbSetup !== "none") { | |
| exitWithError( | |
| "Backend 'none' requires '--db-setup none'. Please remove the --db-setup flag or set it to 'none'.", | |
| ); | |
| } | |
| if (config.serverDeploy && config.serverDeploy !== "none") { | |
| exitWithError( | |
| "Backend 'none' requires '--server-deploy none'. Please remove the --server-deploy flag or set it to 'none'.", | |
| ); | |
| } | |
| } |
🤖 Prompt for AI Agents
In apps/cli/src/utils/config-validation.ts around lines 233-286, the current
checks gate errors on providedFlags (so prompted values bypass them); change the
logic to be value-driven by removing the providedFlags.has() conditions and
simply validate the config fields themselves when backend === "none" (e.g. if
config.runtime !== "none" then exitWithError(...), likewise for database, orm,
api, auth, dbSetup, serverDeploy), ensuring all non-'none' config values cause
the same error regardless of whether they came from flags or interactive
prompts.
| const stackUrl = generateStackUrl(pathname, searchParamsHook); | ||
| const stack = stackState; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Type mismatch risk: ReadonlyURLSearchParams → URLSearchParams.
generateStackUrl expects URLSearchParams, but useSearchParams returns ReadonlyURLSearchParams. This can fail type checking.
Apply at callsite (minimal change):
- const stackUrl = generateStackUrl(pathname, searchParamsHook);
+ const stackUrl = generateStackUrl(
+ pathname,
+ new URLSearchParams(searchParamsHook.toString()),
+ );Alternatively, widen the generateStackUrl signature to accept ReadonlyURLSearchParams.
📝 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.
| const stackUrl = generateStackUrl(pathname, searchParamsHook); | |
| const stack = stackState; | |
| const stackUrl = generateStackUrl( | |
| pathname, | |
| new URLSearchParams(searchParamsHook.toString()), | |
| ); | |
| const stack = stackState; |
🤖 Prompt for AI Agents
In apps/web/src/app/(home)/stack/_components/stack-display.tsx around lines
56-57, generateStackUrl expects a URLSearchParams but searchParamsHook is
ReadonlyURLSearchParams from useSearchParams; convert it before calling
generateStackUrl to satisfy types. Replace the call to
generateStackUrl(pathname, searchParamsHook) with generateStackUrl(pathname, new
URLSearchParams(searchParamsHook.toString())) (or use new
URLSearchParams(Array.from(searchParamsHook.entries())) if you prefer preserving
entries), or alternatively update generateStackUrl's parameter type to accept
ReadonlyURLSearchParams across its usages.
| const tech = options.find( | ||
| (opt) => opt.id === value, | ||
| ); | ||
| return tech ? ( | ||
| <div className="flex items-center gap-2"> | ||
| {tech.icon && ( | ||
| <span className="text-lg">{tech.icon}</span> | ||
| )} | ||
| <span className="text-foreground text-sm"> | ||
| {tech.name} | ||
| </span> | ||
| </div> | ||
| ) : ( | ||
| <span className="text-foreground text-sm"> | ||
| {String(value)} | ||
| </span> | ||
| ); | ||
| })()} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same bug for single-value config. Use TechBadge for consistency.
- const tech = options.find((opt) => opt.id === value);
- return tech ? (
- <div className="flex items-center gap-2">
- {tech.icon && (
- <span className="text-lg">{tech.icon}</span>
- )}
- <span className="text-foreground text-sm">
- {tech.name}
- </span>
- </div>
- ) : (
+ const tech = options.find((opt) => opt.id === value);
+ return tech ? (
+ <TechBadge icon={tech.icon} name={tech.name} category={category} />
+ ) : (
<span className="text-foreground text-sm">
{String(value)}
</span>
);📝 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.
| const tech = options.find( | |
| (opt) => opt.id === value, | |
| ); | |
| return tech ? ( | |
| <div className="flex items-center gap-2"> | |
| {tech.icon && ( | |
| <span className="text-lg">{tech.icon}</span> | |
| )} | |
| <span className="text-foreground text-sm"> | |
| {tech.name} | |
| </span> | |
| </div> | |
| ) : ( | |
| <span className="text-foreground text-sm"> | |
| {String(value)} | |
| </span> | |
| ); | |
| })()} | |
| const tech = options.find((opt) => opt.id === value); | |
| return tech ? ( | |
| <TechBadge icon={tech.icon} name={tech.name} category={category} /> | |
| ) : ( | |
| <span className="text-foreground text-sm"> | |
| {String(value)} | |
| </span> | |
| ); |
🤖 Prompt for AI Agents
In apps/web/src/app/(home)/stack/_components/stack-display.tsx around lines 304
to 321, the single-value rendering duplicates logic used for multi-value techs
and doesn't use the TechBadge component; replace this inline conditional that
looks up options and renders icon/name or value with a single call to TechBadge
so single-value configs render consistently with multi-value ones, passing the
found tech (or a fallback name/string) and ensuring the same props/aria/styling
are used as in the multi-value path.
| window.open( | ||
| `https://twitter.com/intent/tweet?text=${text}&url=${url}`, | ||
| "_blank", | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prevent tabnabbing: add noopener/noreferrer to window.open.
Opening third-party share URLs with "_blank" without "noopener,noreferrer" allows the new page to control the opener.
- window.open(
- `https://twitter.com/intent/tweet?text=${text}&url=${url}`,
- "_blank",
- );
+ window.open(
+ `https://twitter.com/intent/tweet?text=${text}&url=${url}`,
+ "_blank",
+ "noopener,noreferrer",
+ );- window.open(
- `https://www.linkedin.com/sharing/share-offsite/?url=${url}&summary=${text}`,
- "_blank",
- );
+ window.open(
+ `https://www.linkedin.com/sharing/share-offsite/?url=${url}&summary=${text}`,
+ "_blank",
+ "noopener,noreferrer",
+ );Also applies to: 127-130
🤖 Prompt for AI Agents
In apps/web/src/components/ui/share-dialog.tsx around lines 116-119 (and
similarly at 127-130), the window.open calls use "_blank" without protecting
against tabnabbing; update the calls to open the share URL with noopener and
noreferrer by passing those features as the third argument to window.open (e.g.
open(url, "_blank", "noopener,noreferrer")) and after opening set
newWindow.opener = null as a fallback if a reference is returned; this ensures
the opened page cannot access or control the opener.
| const { theme } = useTheme(); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Use resolvedTheme to pick light variants correctly.
useTheme().theme can be "system"; you want the effective theme ("light"/"dark") via resolvedTheme.
- const { theme } = useTheme();
+ const { resolvedTheme } = useTheme();
...
- if (
- theme === "light" &&
+ if (
+ resolvedTheme === "light" &&
(icon.includes("drizzle") ||
icon.includes("prisma") ||
icon.includes("express") ||
icon.includes("clerk"))
) {Also applies to: 71-79
🤖 Prompt for AI Agents
In apps/web/src/components/ui/tech-badge.tsx around lines 58-59 (and also apply
the same change to lines 71-79), the code uses useTheme().theme which may be
"system" and thus doesn't reflect the effective theme; replace usages of theme
with resolvedTheme from useTheme() so you pick the actual "light" or "dark"
variant. Update the hook destructuring to get resolvedTheme, then use
resolvedTheme to select the light/dark badge variants and any conditional
classNames or styles accordingly.
| webFrontend: parseAsArrayOf(parseAsString).withDefault( | ||
| DEFAULT_STACK.webFrontend, | ||
| ), | ||
| nativeFrontend: parseAsArrayOf(parseAsString).withDefault( | ||
| DEFAULT_STACK.nativeFrontend, | ||
| ), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Validate array fields against allowed IDs (not free-form).
webFrontend, nativeFrontend, addons, and examples currently accept arbitrary strings. This allows invalid values to flow into the UI and URLs.
Apply:
- webFrontend: parseAsArrayOf(parseAsString).withDefault(
+ webFrontend: parseAsArrayOf(
+ parseAsStringEnum(getValidIds("webFrontend")),
+ ).withDefault(
DEFAULT_STACK.webFrontend,
),
- nativeFrontend: parseAsArrayOf(parseAsString).withDefault(
+ nativeFrontend: parseAsArrayOf(
+ parseAsStringEnum(getValidIds("nativeFrontend")),
+ ).withDefault(
DEFAULT_STACK.nativeFrontend,
),
- addons: parseAsArrayOf(parseAsString).withDefault(DEFAULT_STACK.addons),
- examples: parseAsArrayOf(parseAsString).withDefault(DEFAULT_STACK.examples),
+ addons: parseAsArrayOf(
+ parseAsStringEnum(getValidIds("addons")),
+ ).withDefault(DEFAULT_STACK.addons),
+ examples: parseAsArrayOf(
+ parseAsStringEnum(getValidIds("examples")),
+ ).withDefault(DEFAULT_STACK.examples),Also applies to: 46-48
🤖 Prompt for AI Agents
In apps/web/src/lib/stack-server.ts around lines 16 to 21 (and similarly at
lines 46 to 48), the array fields webFrontend, nativeFrontend, addons and
examples are currently parsed as free-form strings; change the parsing to
validate each element against the allowed ID sets instead of accepting arbitrary
strings. Replace parseAsArrayOf(parseAsString) with a parser that (a) uses a
whitelist validator (e.g., parseAsArrayOf(parseAsEnum or a custom parse that
checks value ∈ ALLOWED_WEB_FRONTEND_IDS / ALLOWED_NATIVE_FRONTEND_IDS /
ALLOWED_ADDON_IDS / ALLOWED_EXAMPLE_IDS), (b) returns a clear validation error
when an unknown id is encountered, and (c) preserves the same .withDefault(...)
behavior. Apply the same replacement for the corresponding fields at lines 46–48
so all four fields are validated against their allowed ID lists.
| export function parseSearchParamsToStack(searchParams: { | ||
| [key: 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; | ||
| } | ||
| } 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; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Array query parsing may drop values when searchParams provides string[].
If a param appears multiple times (?fe-w=a&fe-w=b), current code keeps only the first. Join arrays before parsing so parseAsArrayOf can handle CSV.
- const parser = stackParsers[stackKey];
- if (parser) {
- const parsedValue = parser.parseServerSide(
- Array.isArray(value) ? value[0] : value,
- );
+ const parser = stackParsers[stackKey];
+ if (parser) {
+ const raw = Array.isArray(value) ? value.join(",") : value;
+ const parsedValue = parser.parseServerSide(raw);
(parsedStack as Record<string, unknown>)[stackKey] = parsedValue;
}📝 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.
| export function parseSearchParamsToStack(searchParams: { | |
| [key: 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; | |
| } | |
| } 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; | |
| } | |
| export function parseSearchParamsToStack(searchParams: { | |
| [key: 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) { | |
| // Join multiple values so CSV-style parsers can handle them | |
| const raw = Array.isArray(value) ? value.join(",") : value; | |
| const parsedValue = parser.parseServerSide(raw); | |
| - const parsedValue = parser.parseServerSide( | |
| - Array.isArray(value) ? value[0] : value, | |
| - ); | |
| (parsedStack as Record<string, unknown>)[stackKey] = parsedValue; | |
| } | |
| } 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; | |
| } |
🤖 Prompt for AI Agents
In apps/web/src/lib/stack-utils.ts around lines 52 to 89, the parsing currently
takes only the first element when a search param is a string[] (e.g.
?fe-w=a&fe-w=b), which drops values; instead, when value is an array join it
into a single CSV/string (e.g. value.join(',')) before passing to
parser.parseServerSide so parseAsArrayOf can consume the combined values; update
the Array.isArray branch to produce a joined string (or appropriate delimiter
used by your parsers) and pass that to parser.parseServerSide, keeping the
try/catch and default-filling logic unchanged.
| 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 projectName = stack.projectName || "my-better-t-app"; | ||
| const flags: string[] = []; | ||
|
|
||
| const isDefaultStack = Object.keys(DEFAULT_STACK).every((key) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Shell-escape project name to handle spaces/special chars.
Unquoted names (e.g., "my app") break the CLI. Quote only when needed.
- const projectName = stack.projectName || "my-better-t-app";
+ const rawName = stack.projectName || "my-better-t-app";
+ const projectName = /\s|["'$\\]/.test(rawName) ? JSON.stringify(rawName) : rawName;(No other changes needed; the return already uses projectName.)
Also applies to: 229-231
🤖 Prompt for AI Agents
In apps/web/src/lib/stack-utils.ts around lines 139 to 156 (and also apply same
fix at lines 229 to 231), the generated CLI command uses an unescaped
projectName which breaks if it contains spaces or special characters; update the
code to shell-escape or quote the projectName only when necessary (e.g., wrap in
single quotes and escape any interior single quotes, or detect safe characters
and leave unquoted) before appending it to the base command so the returned
string is safe to execute in a shell.
abfe0b1 to
bb79f6a
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
apps/web/src/lib/stack-url-state.ts (1)
15-21: Validate array params against TECH_OPTIONS (avoid unknown ids sneaking into state).Currently webFrontend/nativeFrontend/addons/examples accept any string. Tighten to enum-parsed arrays so only supported ids persist from the URL.
- webFrontend: parseAsArrayOf(parseAsString).withDefault( - DEFAULT_STACK.webFrontend, - ), + webFrontend: parseAsArrayOf( + parseAsStringEnum<StackState["webFrontend"][number]>(getValidIds("webFrontend")) + ).withDefault(DEFAULT_STACK.webFrontend), - nativeFrontend: parseAsArrayOf(parseAsString).withDefault( - DEFAULT_STACK.nativeFrontend, - ), + nativeFrontend: parseAsArrayOf( + parseAsStringEnum<StackState["nativeFrontend"][number]>(getValidIds("nativeFrontend")) + ).withDefault(DEFAULT_STACK.nativeFrontend), - addons: parseAsArrayOf(parseAsString).withDefault(DEFAULT_STACK.addons), + addons: parseAsArrayOf( + parseAsStringEnum<StackState["addons"][number]>(getValidIds("addons")) + ).withDefault(DEFAULT_STACK.addons), - examples: parseAsArrayOf(parseAsString).withDefault(DEFAULT_STACK.examples), + examples: parseAsArrayOf( + parseAsStringEnum<StackState["examples"][number]>(getValidIds("examples")) + ).withDefault(DEFAULT_STACK.examples),Also applies to: 18-20, 45-47
apps/web/src/app/(home)/_components/stack-builder.tsx (2)
178-183: Prefer type alias over interface (repo guideline)Switch
interface CompatibilityResultto atypealias.-interface CompatibilityResult { +type CompatibilityResult = { adjustedStack: StackState | null; notes: Record<string, { notes: string[]; hasIssue: boolean }>; changes: Array<{ category: string; message: string }>; -} +};
1173-1181: Use function declaration instead of arrow function for component (repo guideline)Convert component to a named function.
-const StackBuilder = () => { +function StackBuilder() { const [stack, setStack] = useStackStateWithAllParams(); … -}; +}Also consider applying function declarations to other top-level helpers in this file for consistency with the guideline.
♻️ Duplicate comments (3)
apps/web/src/components/ui/share-dialog.tsx (1)
116-120: Prevent tabnabbing: add noopener/noreferrer and nullify opener.- window.open( - `https://twitter.com/intent/tweet?text=${text}&url=${url}`, - "_blank", - ); + const w = window.open( + `https://twitter.com/intent/tweet?text=${text}&url=${url}`, + "_blank", + "noopener,noreferrer", + ); + if (w) w.opener = null; ... - window.open( - `https://www.linkedin.com/sharing/share-offsite/?url=${url}&summary=${text}`, - "_blank", - ); + const w = window.open( + `https://www.linkedin.com/sharing/share-offsite/?url=${url}&summary=${text}`, + "_blank", + "noopener,noreferrer", + ); + if (w) w.opener = null;Also applies to: 127-131
apps/web/src/lib/stack-utils.ts (2)
69-75: Bug: multi-valued params drop values (only first kept). Join arrays before parsing.- const parsedValue = parser.parseServerSide( - Array.isArray(value) ? value[0] : value, - ); + const raw = Array.isArray(value) ? value.join(",") : value; + const parsedValue = parser.parseServerSide(raw);
153-160: Shell-escape project name to handle spaces/special chars in generated CLI.- const projectName = stack.projectName || "my-better-t-app"; + const rawName = stack.projectName || "my-better-t-app"; + const projectName = /\s|["'$\\]/.test(rawName) ? JSON.stringify(rawName) : rawName; ... - return `${base} ${projectName}${flags.length > 0 ? ` ${flags.join(" ")}` : ""}`; + return `${base} ${projectName}${flags.length > 0 ? ` ${flags.join(" ")}` : ""}`;Also applies to: 229-231
🧹 Nitpick comments (10)
apps/web/src/components/ui/kibo-ui/qr-code/index.tsx (1)
31-39: Prefer function declarations per guidelines. Also trim CSS var values and avoid setState-after-unmount.-export const QRCode = ({ +export function QRCode({ data, foreground, background, robustness = "M", className, ...props -}: QRCodeProps) => { +}: QRCodeProps) { const [svg, setSVG] = useState<string | null>(null); useEffect(() => { - const generateQR = async () => { + let disposed = false; + const generateQR = async () => { try { const styles = getComputedStyle(document.documentElement); - const foregroundColor = - foreground ?? styles.getPropertyValue("--foreground"); - const backgroundColor = - background ?? styles.getPropertyValue("--background"); + const foregroundColor = (foreground ?? styles.getPropertyValue("--foreground")).trim(); + const backgroundColor = (background ?? styles.getPropertyValue("--background")).trim(); ... - setSVG(newSvg); + if (!disposed) setSVG(newSvg); } catch (err) { console.error(err); } }; generateQR(); - }, [data, foreground, background, robustness]); + return () => { + disposed = true; + }; + }, [data, foreground, background, robustness]); if (!svg) { return null; } return ( <div className={cn("size-full", "[&_svg]:size-full", className)} // biome-ignore lint/security/noDangerouslySetInnerHtml: "Required for SVG" dangerouslySetInnerHTML={{ __html: svg }} {...props} /> ); -}; +}Also applies to: 80-88
apps/web/src/components/ui/share-dialog.tsx (2)
30-34: Follow repo guideline: prefer type alias over interface for props.-interface ShareDialogProps { +type ShareDialogProps = { children: React.ReactNode; stackUrl: string; stackState: StackState; -} +}
100-109: Avoid setState after unmount for the “copied” timeout.Store timeout id in a ref and clear it in a cleanup useEffect when the dialog unmounts.
apps/web/src/lib/stack-utils.ts (3)
20-22: Deduplicate helpers: getValidIds and CATEGORY_ORDER exist here and in stack-url-state.Export once (e.g., from stack-url-state) and import elsewhere to avoid divergence.
Also applies to: 24-41
57-65: Consider ignoring all utm_ keys, not just three hard-coded ones.*Filter by key.startsWith("utm_") to keep the parser future-proof.
282-309: Arrow functions inside TSX: optional style tweak.Repo guideline prefers function declarations; consider converting inner arrows (e.g., setStackWithAllParams, setStack) for consistency.
Also applies to: 435-465
apps/web/src/app/(home)/_components/stack-builder.tsx (4)
2085-2098: Avoid double computation of disabled reason per optionYou call
isOptionCompatible(which callsgetDisabledReason) and then callgetDisabledReasonagain. Compute once and derive both values to cut the work by ~50% in this hot render path.-const isDisabled = !isOptionCompatible( - stack, - categoryKey as keyof typeof TECH_OPTIONS, - tech.id, -); - -const disabledReason = isDisabled - ? getDisabledReason( - stack, - categoryKey as keyof typeof TECH_OPTIONS, - tech.id, - ) - : null; +const disabledReason = getDisabledReason( + stack, + categoryKey as keyof typeof TECH_OPTIONS, + tech.id, +); +const isDisabled = disabledReason !== null;
1512-1845: Heavy per-option compute; consider cheap caching for this render
getDisabledReasondeep-clones state and re-runs compatibility analysis per option. Combined with the previous double call, this is expensive. Keep a per-render Map cache keyed by${category}:${optionId}to memoize results, or precompute reasons per category once.
263-266: Remove dead branchEmpty if/else block can be removed.
- if (nextStack.nativeFrontend[0] === "none") { - } else { - }
1470-1474: Clipboard copy lacks error handlingMirror the try/catch used in ShareDialog to avoid silent failures (e.g., denied permission).
-const copyToClipboard = () => { - navigator.clipboard.writeText(command); - setCopied(true); - setTimeout(() => setCopied(false), 2000); -}; +const copyToClipboard = async () => { + try { + await navigator.clipboard.writeText(command); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch { + toast.error("Failed to copy command"); + } +};
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (16)
apps/cli/src/prompts/config-prompts.ts(0 hunks)apps/cli/src/utils/config-validation.ts(2 hunks)apps/web/lib/cn.ts(0 hunks)apps/web/package.json(3 hunks)apps/web/scripts/generate-analytics.ts(0 hunks)apps/web/src/app/(home)/_components/stack-builder.tsx(19 hunks)apps/web/src/app/(home)/stack/_components/stack-display.tsx(1 hunks)apps/web/src/app/(home)/stack/page.tsx(1 hunks)apps/web/src/components/ai/page-actions.tsx(1 hunks)apps/web/src/components/ui/kibo-ui/qr-code/index.tsx(1 hunks)apps/web/src/components/ui/kibo-ui/qr-code/server.tsx(1 hunks)apps/web/src/components/ui/share-dialog.tsx(1 hunks)apps/web/src/components/ui/tech-badge.tsx(1 hunks)apps/web/src/lib/stack-server.ts(1 hunks)apps/web/src/lib/stack-url-state.ts(1 hunks)apps/web/src/lib/stack-utils.ts(1 hunks)
💤 Files with no reviewable changes (3)
- apps/web/lib/cn.ts
- apps/cli/src/prompts/config-prompts.ts
- apps/web/scripts/generate-analytics.ts
🚧 Files skipped from review as they are similar to previous changes (7)
- apps/web/src/components/ai/page-actions.tsx
- apps/web/src/lib/stack-server.ts
- apps/web/package.json
- apps/cli/src/utils/config-validation.ts
- apps/web/src/app/(home)/stack/page.tsx
- apps/web/src/components/ui/tech-badge.tsx
- apps/web/src/app/(home)/stack/_components/stack-display.tsx
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/convex_rules.mdc)
**/*.{ts,tsx}: Use Id from './_generated/dataModel' to type document ids (e.g., Id<'users'>)
Ensure Record key/value types align with validators (e.g., v.record(v.id('users'), v.string()) => Record<Id<'users'>, string>)
Be strict with types for document ids; prefer Id<'table'> over string
Use 'as const' for string literals in discriminated unions
When using Array and Record types, declare with explicit generic types (e.g., const arr: Array = ...)In TypeScript, use type aliases (type) instead of interface for type definitions
Files:
apps/web/src/lib/stack-url-state.tsapps/web/src/components/ui/share-dialog.tsxapps/web/src/lib/stack-utils.tsapps/web/src/app/(home)/_components/stack-builder.tsxapps/web/src/components/ui/kibo-ui/qr-code/index.tsxapps/web/src/components/ui/kibo-ui/qr-code/server.tsx
**/*.{js,jsx,ts,tsx,mjs,cjs}
📄 CodeRabbit inference engine (.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc)
**/*.{js,jsx,ts,tsx,mjs,cjs}: Do not use dotenv; Bun auto-loads .env
UseBun.serve()for HTTP/WebSockets; do not useexpress
Usebun:sqlitefor SQLite; do not usebetter-sqlite3
UseBun.redisfor Redis; do not useioredis
UseBun.sqlfor Postgres; do not usepgorpostgres.js
Use built-inWebSocket; do not usews
PreferBun.fileovernode:fsreadFile/writeFile
UseBun.$instead ofexecafor shelling out
Files:
apps/web/src/lib/stack-url-state.tsapps/web/src/components/ui/share-dialog.tsxapps/web/src/lib/stack-utils.tsapps/web/src/app/(home)/_components/stack-builder.tsxapps/web/src/components/ui/kibo-ui/qr-code/index.tsxapps/web/src/components/ui/kibo-ui/qr-code/server.tsx
**/*.{js,jsx,ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/better-t-stack-repo.mdc)
Use normal (function declaration) syntax; do not use arrow functions
Files:
apps/web/src/lib/stack-url-state.tsapps/web/src/components/ui/share-dialog.tsxapps/web/src/lib/stack-utils.tsapps/web/src/app/(home)/_components/stack-builder.tsxapps/web/src/components/ui/kibo-ui/qr-code/index.tsxapps/web/src/components/ui/kibo-ui/qr-code/server.tsx
🧬 Code graph analysis (5)
apps/web/src/components/ui/share-dialog.tsx (5)
apps/web/src/lib/constant.ts (2)
StackState(707-725)TECH_OPTIONS(5-582)apps/web/src/lib/stack-utils.ts (1)
CATEGORY_ORDER(467-467)apps/web/src/components/ui/tech-badge.tsx (1)
TechBadge(94-109)apps/web/src/components/ui/kibo-ui/qr-code/index.tsx (1)
QRCode(31-88)apps/web/src/components/ui/kibo-ui/qr-code/server.tsx (1)
QRCode(12-42)
apps/web/src/lib/stack-utils.ts (2)
apps/web/src/lib/constant.ts (4)
TECH_OPTIONS(5-582)StackState(707-725)DEFAULT_STACK(727-745)isStackDefault(747-796)apps/web/src/lib/stack-url-state.ts (3)
stackUrlKeys(62-80)stackParsers(13-60)stackQueryStatesOptions(82-87)
apps/web/src/app/(home)/_components/stack-builder.tsx (3)
apps/web/src/lib/stack-utils.ts (3)
useStackStateWithAllParams(282-309)generateStackUrlFromState(246-280)generateStackCommand(139-232)apps/web/src/lib/constant.ts (3)
StackState(707-725)DEFAULT_STACK(727-745)TECH_OPTIONS(5-582)apps/web/src/components/ui/share-dialog.tsx (1)
ShareDialog(36-235)
apps/web/src/components/ui/kibo-ui/qr-code/index.tsx (1)
apps/web/src/components/ui/kibo-ui/qr-code/server.tsx (2)
QRCodeProps(5-10)QRCode(12-42)
apps/web/src/components/ui/kibo-ui/qr-code/server.tsx (1)
apps/web/src/components/ui/kibo-ui/qr-code/index.tsx (2)
QRCodeProps(8-13)QRCode(31-88)
🪛 ast-grep (0.38.6)
apps/web/src/components/ui/kibo-ui/qr-code/index.tsx
[warning] 83-83: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html
(react-unsafe-html-injection)
apps/web/src/components/ui/kibo-ui/qr-code/server.tsx
[warning] 37-37: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html
(react-unsafe-html-injection)
🔇 Additional comments (4)
apps/web/src/lib/stack-url-state.ts (1)
82-87: LGTM: clear-on-default is the right behavior for sharable URLs.Setting clearOnDefault: true keeps URLs tidy and predictable while still round-tripping state.
apps/web/src/components/ui/kibo-ui/qr-code/server.tsx (1)
37-38: On XSS risk: acceptable here given library-generated SVG.Input “data” does not flow into the SVG markup; QR.toString produces shapes only. Keep this, but ensure callers pass URLs (as done in ShareDialog).
Would you like a small runtime guard to enforce http/https URLs for data?
apps/web/src/components/ui/kibo-ui/qr-code/index.tsx (1)
83-85: On dangerouslySetInnerHTML: acceptable with QR.toString.Same rationale as server component; keep as-is.
apps/web/src/app/(home)/_components/stack-builder.tsx (1)
367-376: DB Setup messaging is accurate — thedbSetupoption withid: "none"is named “Basic Setup” inconstant.ts, so settingdbSetup = "none"aligns with the UI label.
| const getStackUrl = (): string => { | ||
| return generateStackUrlFromState(stack); | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Share URL should use the compatibility-adjusted state
Sharing the raw state can produce a URL that re-adjusts on open. Generate the URL from the adjusted state for deterministic sharing.
-const getStackUrl = (): string => {
- return generateStackUrlFromState(stack);
-};
+const getStackUrl = (): string => {
+ const shareState = compatibilityAnalysis.adjustedStack ?? stack;
+ return generateStackUrlFromState(shareState);
+};📝 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.
| const getStackUrl = (): string => { | |
| return generateStackUrlFromState(stack); | |
| }; | |
| const getStackUrl = (): string => { | |
| const shareState = compatibilityAnalysis.adjustedStack ?? stack; | |
| return generateStackUrlFromState(shareState); | |
| }; |
🤖 Prompt for AI Agents
In apps/web/src/app/(home)/_components/stack-builder.tsx around lines 1241-1243,
the code builds the share URL from the raw stack state which can lead to
non-deterministic re-adjustment on open; instead compute the
compatibility-adjusted state first (using the existing compatibility/adjustment
helper used elsewhere in this file or module), ensure it does not mutate the
original stack, then pass that adjusted state into generateStackUrlFromState so
the shared URL is generated from the adjusted/deterministic state.
| export type QRCodeProps = HTMLAttributes<HTMLDivElement> & { | ||
| data: string; | ||
| foreground: string; | ||
| background: string; | ||
| robustness?: "L" | "M" | "Q" | "H"; | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Unify server/client QRCode API; make colors optional and align rendering options.
Client QRCode makes colors optional and sets margin: 0; server requires colors and omits margin, leading to API friction and visual mismatch.
-export type QRCodeProps = HTMLAttributes<HTMLDivElement> & {
- data: string;
- foreground: string;
- background: string;
- robustness?: "L" | "M" | "Q" | "H";
-};
+export type QRCodeProps = HTMLAttributes<HTMLDivElement> & {
+ data: string;
+ foreground?: string;
+ background?: string;
+ robustness?: "L" | "M" | "Q" | "H";
+};
-export const QRCode = async ({
- data,
- foreground,
- background,
- robustness = "M",
- className,
- ...props
-}: QRCodeProps) => {
- const svg = await QR.toString(data, {
+export async function QRCode({
+ data,
+ foreground,
+ background,
+ robustness = "M",
+ className,
+ ...props
+}: QRCodeProps) {
+ const svg = await QR.toString(data, {
type: "svg",
color: {
- dark: foreground,
- light: background,
+ dark: foreground ?? "#0a0a0a",
+ light: background ?? "#ffffff",
},
width: 200,
errorCorrectionLevel: robustness,
+ margin: 0,
});
if (!svg) {
throw new Error("Failed to generate QR code");
}
return (
<div
className={cn("size-full", "[&_svg]:size-full", className)}
// biome-ignore lint/security/noDangerouslySetInnerHtml: "Required for SVG"
dangerouslySetInnerHTML={{ __html: svg }}
{...props}
/>
);
-};
+}Also applies to: 12-29
🤖 Prompt for AI Agents
In apps/web/src/components/ui/kibo-ui/qr-code/server.tsx around lines 5-10 and
12-29, the server QRCode prop types require foreground/background and omit
margin while the client QRCode makes colors optional and uses margin: 0, causing
API and render differences; change the server prop type so foreground and
background are optional (match client), add a default margin: 0 in server
rendering, and align any other rendering option names (e.g., robustness ->
errorCorrectionLevel or vice versa) with the client API; ensure defaults are
applied on the server side when props are undefined so both imports accept the
same props and produce visually identical output.
Summary by CodeRabbit
New Features
Improvements
Bug Fixes
Chores