diff --git a/apps/cli/src/prompts/config-prompts.ts b/apps/cli/src/prompts/config-prompts.ts index cc20b58..c6fa2be 100644 --- a/apps/cli/src/prompts/config-prompts.ts +++ b/apps/cli/src/prompts/config-prompts.ts @@ -123,25 +123,6 @@ export async function gatherConfig( }, ); - if (result.backend === "convex") { - result.runtime = "none"; - result.database = "none"; - result.orm = "none"; - result.api = "none"; - result.dbSetup = "none"; - result.examples = ["todo"]; - } - - if (result.backend === "none") { - result.runtime = "none"; - result.database = "none"; - result.orm = "none"; - result.api = "none"; - result.auth = "none"; - result.dbSetup = "none"; - result.examples = []; - } - return { projectName: projectName, projectDir: projectDir, diff --git a/apps/cli/src/utils/config-validation.ts b/apps/cli/src/utils/config-validation.ts index 9505956..d4e9153 100644 --- a/apps/cli/src/utils/config-validation.ts +++ b/apps/cli/src/utils/config-validation.ts @@ -175,6 +175,116 @@ export function validateDatabaseSetup( } } +export function validateConvexConstraints( + config: Partial, + providedFlags: Set, +): 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 validateBackendNoneConstraints( + config: Partial, + providedFlags: Set, +): 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 validateBackendConstraints( config: Partial, providedFlags: Set, @@ -201,16 +311,6 @@ export function validateBackendConstraints( } } - if ( - backend === "convex" && - config.auth === "better-auth" && - providedFlags.has("auth") - ) { - exitWithError( - "Better-Auth is not compatible with the Convex backend. Please use '--auth clerk' or '--auth none'.", - ); - } - if ( providedFlags.has("backend") && backend && @@ -287,6 +387,8 @@ export function validateFullConfig( validateDatabaseOrmAuth(config, providedFlags); validateDatabaseSetup(config, providedFlags); + validateConvexConstraints(config, providedFlags); + validateBackendNoneConstraints(config, providedFlags); validateBackendConstraints(config, providedFlags, options); validateFrontendConstraints(config, providedFlags); diff --git a/apps/web/lib/cn.ts b/apps/web/lib/cn.ts deleted file mode 100644 index 8e473da..0000000 --- a/apps/web/lib/cn.ts +++ /dev/null @@ -1 +0,0 @@ -export { twMerge as cn } from "tailwind-merge"; diff --git a/apps/web/package.json b/apps/web/package.json index a53b736..867c3db 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -27,6 +27,7 @@ "clsx": "^2.1.1", "convex": "^1.25.4", "convex-helpers": "^0.1.104", + "culori": "^4.0.2", "date-fns": "^4.1.0", "fumadocs-core": "15.6.7", "fumadocs-mdx": "11.7.3", @@ -38,6 +39,7 @@ "nuqs": "^2.4.3", "papaparse": "^5.5.3", "posthog-js": "^1.258.5", + "qrcode": "^1.5.4", "radix-ui": "^1.4.2", "react": "^19.1.1", "react-dom": "^19.1.1", @@ -52,9 +54,11 @@ }, "devDependencies": { "@tailwindcss/postcss": "^4.1.11", + "@types/culori": "^4.0.0", "@types/mdx": "^2.0.13", "@types/node": "24.1.0", "@types/papaparse": "^5.3.16", + "@types/qrcode": "^1.5.5", "@types/react": "^19.1.9", "@types/react-dom": "^19.1.7", "eslint": "^9.32.0", diff --git a/apps/web/scripts/generate-analytics.ts b/apps/web/scripts/generate-analytics.ts index 6b42590..1a05ff1 100644 --- a/apps/web/scripts/generate-analytics.ts +++ b/apps/web/scripts/generate-analytics.ts @@ -242,23 +242,19 @@ async function generateAnalyticsData() { cliVersionCounts[cliVersion] = (cliVersionCounts[cliVersion] || 0) + 1; - // Auth const auth = row["*.properties.auth"] === "True" ? "enabled" : "disabled"; authCounts[auth] = (authCounts[auth] || 0) + 1; if (auth === "enabled") authEnabledCount++; - // Git const git = row["*.properties.git"] === "True" ? "enabled" : "disabled"; gitCounts[git] = (gitCounts[git] || 0) + 1; - // Install const install = row["*.properties.install"] === "True" ? "enabled" : "disabled"; installCounts[install] = (installCounts[install] || 0) + 1; - // Examples const examples = [ row["*.properties.examples.0"], row["*.properties.examples.1"], @@ -271,7 +267,6 @@ async function generateAnalyticsData() { } } - // Addons const addons = [ row["*.properties.addons.0"], row["*.properties.addons.1"], @@ -288,23 +283,19 @@ async function generateAnalyticsData() { } } - // Runtime const runtime = row["*.properties.runtime"] || "unknown"; runtimeCounts[runtime] = (runtimeCounts[runtime] || 0) + 1; - // Web Deploy (migrate "workers" to "wrangler") const webDeploy = row["*.properties.webDeploy"] || "none"; const normalizedWebDeploy = webDeploy === "workers" ? "wrangler" : webDeploy; webDeployCounts[normalizedWebDeploy] = (webDeployCounts[normalizedWebDeploy] || 0) + 1; - // Server Deploy const serverDeploy = row["*.properties.serverDeploy"] || "none"; serverDeployCounts[serverDeploy] = (serverDeployCounts[serverDeploy] || 0) + 1; - // Project type const hasFrontend = (frontend0 && frontend0 !== "none") || (frontend1 && frontend1 !== "none"); @@ -321,7 +312,6 @@ async function generateAnalyticsData() { } projectTypeCounts[type] = (projectTypeCounts[type] || 0) + 1; - // Stack combinations const frontends = [frontend0, frontend1].filter( (f) => f && f !== "none" && f !== "", ); @@ -332,7 +322,6 @@ async function generateAnalyticsData() { const combo = parts.length > 0 ? parts.join(" + ") : "none"; stackComboCounts[combo] = (stackComboCounts[combo] || 0) + 1; - // Database + ORM combinations if (database !== "none" && orm !== "none") { const combo = `${database} + ${orm}`; dbORMComboCounts[combo] = (dbORMComboCounts[combo] || 0) + 1; diff --git a/apps/web/src/app/(home)/_components/stack-builder.tsx b/apps/web/src/app/(home)/_components/stack-builder.tsx index afb2b4b..546c8f8 100644 --- a/apps/web/src/app/(home)/_components/stack-builder.tsx +++ b/apps/web/src/app/(home)/_components/stack-builder.tsx @@ -2,6 +2,7 @@ import { Check, + ChevronDown, ClipboardCopy, InfoIcon, RefreshCw, @@ -10,15 +11,23 @@ import { Shuffle, Star, Terminal, + Zap, } from "lucide-react"; import { motion } from "motion/react"; import Image from "next/image"; import { useTheme } from "next-themes"; -import { useQueryStates } from "nuqs"; + import type React from "react"; -import { useEffect, useMemo, useRef, useState } from "react"; +import { startTransition, useEffect, useMemo, useRef, useState } from "react"; import { toast } from "sonner"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; import { ScrollArea } from "@/components/ui/scroll-area"; +import { ShareDialog } from "@/components/ui/share-dialog"; import { Tooltip, TooltipContent, @@ -27,12 +36,16 @@ import { } from "@/components/ui/tooltip"; import { DEFAULT_STACK, - isStackDefault, PRESET_TEMPLATES, type StackState, TECH_OPTIONS, } from "@/lib/constant"; -import { stackParsers, stackQueryStatesOptions } from "@/lib/stack-url-state"; +import { + CATEGORY_ORDER, + generateStackCommand, + generateStackUrlFromState, + useStackStateWithAllParams, +} from "@/lib/stack-utils"; import { cn } from "@/lib/utils"; const validateProjectName = (name: string): string | undefined => { @@ -60,25 +73,6 @@ const validateProjectName = (name: string): string | undefined => { return undefined; }; -const CATEGORY_ORDER: Array = [ - "webFrontend", - "nativeFrontend", - "backend", - "runtime", - "api", - "database", - "orm", - "dbSetup", - "webDeploy", - "serverDeploy", - "auth", - "packageManager", - "addons", - "examples", - "git", - "install", -]; - const hasPWACompatibleFrontend = (webFrontend: string[]) => webFrontend.some((f) => ["tanstack-router", "react-router", "solid", "next"].includes(f), @@ -1137,6 +1131,38 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => { } } + if ( + nextStack.serverDeploy === "alchemy" && + (nextStack.runtime !== "workers" || nextStack.backend !== "hono") + ) { + notes.serverDeploy.notes.push( + "Alchemy deployment requires Cloudflare Workers runtime and Hono backend. Server deployment disabled.", + ); + notes.serverDeploy.notes.push( + "To use Alchemy: Set Runtime to 'Cloudflare Workers' and Backend to 'Hono', then re-enable Alchemy deployment.", + ); + if (nextStack.runtime !== "workers") { + notes.runtime.notes.push( + "Selected runtime is not compatible with Alchemy deployment. Switch to 'Cloudflare Workers' to use Alchemy.", + ); + } + if (nextStack.backend !== "hono") { + notes.backend.notes.push( + "Selected backend is not compatible with Alchemy deployment. Switch to 'Hono' to use Alchemy.", + ); + } + notes.serverDeploy.hasIssue = true; + notes.runtime.hasIssue = true; + notes.backend.hasIssue = true; + nextStack.serverDeploy = "none"; + changed = true; + changes.push({ + category: "serverDeploy", + message: + "Server deployment disabled (Tip: Use Cloudflare Workers runtime + Hono backend to enable Alchemy)", + }); + } + return { adjustedStack: changed ? nextStack : null, notes, @@ -1144,114 +1170,8 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => { }; }; -const generateCommand = (stackState: StackState): string => { - let base: string; - switch (stackState.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 = stackState.projectName || "my-better-t-app"; - const flags: string[] = []; - - const isDefaultStack = Object.keys(DEFAULT_STACK).every((key) => { - if (key === "projectName") return true; - const defaultKey = key as keyof StackState; - return isStackDefault(stackState, defaultKey, stackState[defaultKey]); - }); - - if (isDefaultStack) { - flags.push("--yes"); - } else { - const combinedFrontends = [ - ...stackState.webFrontend, - ...stackState.nativeFrontend, - ].filter((v, _, arr) => v !== "none" || arr.length === 1); - - if (combinedFrontends.length === 0 || combinedFrontends[0] === "none") { - flags.push("--frontend none"); - } else { - flags.push(`--frontend ${combinedFrontends.join(" ")}`); - } - - flags.push(`--backend ${stackState.backend}`); - - flags.push(`--runtime ${stackState.runtime}`); - - flags.push(`--api ${stackState.api}`); - - flags.push(`--auth ${stackState.auth}`); - - flags.push(`--database ${stackState.database}`); - - flags.push(`--orm ${stackState.orm}`); - - flags.push(`--db-setup ${stackState.dbSetup}`); - - flags.push(`--package-manager ${stackState.packageManager}`); - - if (stackState.git === "false") { - flags.push("--no-git"); - } else { - flags.push("--git"); - } - - flags.push(`--web-deploy ${stackState.webDeploy}`); - - flags.push(`--server-deploy ${stackState.serverDeploy}`); - - if (stackState.install === "false") { - flags.push("--no-install"); - } else { - flags.push("--install"); - } - - if (stackState.addons.length > 0) { - const validAddons = stackState.addons.filter((addon) => - [ - "pwa", - "tauri", - "starlight", - "biome", - "husky", - "turborepo", - "ultracite", - "fumadocs", - "oxlint", - "ruler", - ].includes(addon), - ); - if (validAddons.length > 0) { - flags.push(`--addons ${validAddons.join(" ")}`); - } - } else { - flags.push("--addons none"); - } - - if (stackState.examples.length > 0) { - flags.push(`--examples ${stackState.examples.join(" ")}`); - } else { - flags.push("--examples none"); - } - } - - return `${base} ${projectName}${ - flags.length > 0 ? ` ${flags.join(" ")}` : "" - }`; -}; - const StackBuilder = () => { - const [stack, setStack] = useQueryStates( - stackParsers, - stackQueryStatesOptions, - ); + const [stack, setStack] = useStackStateWithAllParams(); const [command, setCommand] = useState(""); const [copied, setCopied] = useState(false); @@ -1265,6 +1185,7 @@ const StackBuilder = () => { const sectionRefs = useRef>({}); const contentRef = useRef(null); + const lastAppliedStackString = useRef(""); const compatibilityAnalysis = useMemo( () => analyzeStackCompatibility(stack), @@ -1310,72 +1231,15 @@ const StackBuilder = () => { (randomStack[catKey] as string) = options[randomIndex].id; } } - setStack(randomStack as StackState); + startTransition(() => { + setStack(randomStack as StackState); + }); contentRef.current?.scrollTo(0, 0); toast.success("Random stack generated!"); }; - const shareToTwitter = () => { - const getStackSummary = (): string => { - const selectedTechs: string[] = []; - - for (const category of CATEGORY_ORDER) { - const categoryKey = category as keyof StackState; - const options = TECH_OPTIONS[category as keyof typeof TECH_OPTIONS]; - const selectedValue = stack[categoryKey]; - - if (!options) continue; - - if (Array.isArray(selectedValue)) { - if ( - selectedValue.length === 0 || - (selectedValue.length === 1 && selectedValue[0] === "none") - ) { - continue; - } - - for (const id of selectedValue) { - if (id === "none") continue; - const tech = options.find((opt) => opt.id === id); - if (tech) { - selectedTechs.push(tech.name); - } - } - } else { - const tech = options.find((opt) => opt.id === selectedValue); - if ( - !tech || - tech.id === "none" || - tech.id === "false" || - ((category === "git" || - category === "install" || - category === "auth") && - tech.id === "true") - ) { - continue; - } - selectedTechs.push(tech.name); - } - } - - return selectedTechs.length > 0 - ? selectedTechs.join(" • ") - : "Custom stack"; - }; - - const stackSummary = getStackSummary(); - const text = encodeURIComponent( - `Check out this cool tech stack I configured with Create Better T Stack!\n\nšŸš€ ${stackSummary}\n\n`, - ); - if (typeof window !== "undefined") { - const url = encodeURIComponent(window.location.href); - window.open( - `https://twitter.com/intent/tweet?text=${text}&url=${url}`, - "_blank", - ); - } else { - toast.error("Could not generate share link."); - } + const getStackUrl = (): string => { + return generateStackUrlFromState(stack); }; const selectedBadges = (() => { @@ -1464,41 +1328,45 @@ const StackBuilder = () => { useEffect(() => { if (compatibilityAnalysis.adjustedStack) { - if (compatibilityAnalysis.changes.length > 0) { - if (compatibilityAnalysis.changes.length === 1) { - toast.info(compatibilityAnalysis.changes[0].message, { - duration: 4000, - }); - } else if (compatibilityAnalysis.changes.length > 1) { - const message = `${ - compatibilityAnalysis.changes.length - } compatibility adjustments made:\n${compatibilityAnalysis.changes - .map((c) => `• ${c.message}`) - .join("\n")}`; - toast.info(message, { - duration: 5000, - }); - } - } - setLastChanges(compatibilityAnalysis.changes); + const adjustedStackString = JSON.stringify( + compatibilityAnalysis.adjustedStack, + ); - const isStackDifferent = - JSON.stringify(stack) !== - JSON.stringify(compatibilityAnalysis.adjustedStack); - if (isStackDifferent) { - setStack(compatibilityAnalysis.adjustedStack); + if (lastAppliedStackString.current !== adjustedStackString) { + startTransition(() => { + if (compatibilityAnalysis.changes.length > 0) { + if (compatibilityAnalysis.changes.length === 1) { + toast.info(compatibilityAnalysis.changes[0].message, { + duration: 4000, + }); + } else if (compatibilityAnalysis.changes.length > 1) { + const message = `${ + compatibilityAnalysis.changes.length + } compatibility adjustments made:\n${compatibilityAnalysis.changes + .map((c) => `• ${c.message}`) + .join("\n")}`; + toast.info(message, { + duration: 5000, + }); + } + } + setLastChanges(compatibilityAnalysis.changes); + if (compatibilityAnalysis.adjustedStack) { + setStack(compatibilityAnalysis.adjustedStack); + } + lastAppliedStackString.current = adjustedStackString; + }); } } }, [ compatibilityAnalysis.adjustedStack, - setStack, compatibilityAnalysis.changes, - stack, + setStack, ]); useEffect(() => { const stackToUse = compatibilityAnalysis.adjustedStack || stack; - const cmd = generateCommand(stackToUse); + const cmd = generateStackCommand(stackToUse); setCommand(cmd); }, [stack, compatibilityAnalysis.adjustedStack]); @@ -1514,86 +1382,88 @@ const StackBuilder = () => { return; } - setStack((currentStack) => { - const catKey = category as keyof StackState; - const update: Partial = {}; - const currentValue = currentStack[catKey]; + startTransition(() => { + setStack((currentStack) => { + const catKey = category as keyof StackState; + const update: Partial = {}; + const currentValue = currentStack[catKey]; - if ( - catKey === "webFrontend" || - catKey === "nativeFrontend" || - catKey === "addons" || - catKey === "examples" - ) { - const currentArray = Array.isArray(currentValue) - ? [...currentValue] - : []; - let nextArray = [...currentArray]; - const isSelected = currentArray.includes(techId); + if ( + catKey === "webFrontend" || + catKey === "nativeFrontend" || + catKey === "addons" || + catKey === "examples" + ) { + const currentArray = Array.isArray(currentValue) + ? [...currentValue] + : []; + let nextArray = [...currentArray]; + const isSelected = currentArray.includes(techId); - if (catKey === "webFrontend") { - if (techId === "none") { - nextArray = ["none"]; - } else if (isSelected) { - if (currentArray.length > 1) { - nextArray = nextArray.filter((id) => id !== techId); - } else { + if (catKey === "webFrontend") { + if (techId === "none") { nextArray = ["none"]; + } else if (isSelected) { + if (currentArray.length > 1) { + nextArray = nextArray.filter((id) => id !== techId); + } else { + nextArray = ["none"]; + } + } else { + nextArray = [techId]; + } + } else if (catKey === "nativeFrontend") { + if (techId === "none") { + nextArray = ["none"]; + } else if (isSelected) { + nextArray = ["none"]; + } else { + nextArray = [techId]; } } else { - nextArray = [techId]; + if (isSelected) { + nextArray = nextArray.filter((id) => id !== techId); + } else { + nextArray.push(techId); + } + if (nextArray.length > 1) { + nextArray = nextArray.filter((id) => id !== "none"); + } + if ( + nextArray.length === 0 && + (catKey === "addons" || catKey === "examples") + ) { + } else if (nextArray.length === 0) { + nextArray = ["none"]; + } } - } else if (catKey === "nativeFrontend") { - if (techId === "none") { - nextArray = ["none"]; - } else if (isSelected) { - nextArray = ["none"]; - } else { - nextArray = [techId]; + + const uniqueNext = [...new Set(nextArray)].sort(); + const uniqueCurrent = [...new Set(currentArray)].sort(); + + if (JSON.stringify(uniqueNext) !== JSON.stringify(uniqueCurrent)) { + update[catKey] = uniqueNext; } } else { - if (isSelected) { - nextArray = nextArray.filter((id) => id !== techId); + if (currentValue !== techId) { + update[catKey] = techId; } else { - nextArray.push(techId); - } - if (nextArray.length > 1) { - nextArray = nextArray.filter((id) => id !== "none"); - } - if ( - nextArray.length === 0 && - (catKey === "addons" || catKey === "examples") - ) { - } else if (nextArray.length === 0) { - nextArray = ["none"]; + if ( + (category === "git" || category === "install") && + techId === "false" + ) { + update[catKey] = "true"; + } else if ( + (category === "git" || category === "install") && + techId === "true" + ) { + update[catKey] = "false"; + } } } - const uniqueNext = [...new Set(nextArray)].sort(); - const uniqueCurrent = [...new Set(currentArray)].sort(); - - if (JSON.stringify(uniqueNext) !== JSON.stringify(uniqueCurrent)) { - update[catKey] = uniqueNext; - } - } else { - if (currentValue !== techId) { - update[catKey] = techId; - } else { - if ( - (category === "git" || category === "install") && - techId === "false" - ) { - update[catKey] = "true"; - } else if ( - (category === "git" || category === "install") && - techId === "true" - ) { - update[catKey] = "false"; - } - } - } - - return Object.keys(update).length > 0 ? update : {}; + return Object.keys(update).length > 0 ? update : {}; + }); }); }; @@ -1604,7 +1474,9 @@ const StackBuilder = () => { }; const resetStack = () => { - setStack(DEFAULT_STACK); + startTransition(() => { + setStack(DEFAULT_STACK); + }); contentRef.current?.scrollTo(0, 0); }; @@ -1616,7 +1488,9 @@ const StackBuilder = () => { const loadSavedStack = () => { if (lastSavedStack) { - setStack(lastSavedStack); + startTransition(() => { + setStack(lastSavedStack); + }); contentRef.current?.scrollTo(0, 0); toast.success("Saved configuration loaded"); } @@ -1627,17 +1501,64 @@ const StackBuilder = () => { (template) => template.id === presetId, ); if (preset) { - setStack(preset.stack); + startTransition(() => { + setStack(preset.stack); + }); contentRef.current?.scrollTo(0, 0); toast.success(`Applied preset: ${preset.name}`); } }; - const isOptionCompatible = ( + const getDisabledReason = ( currentStack: StackState, category: keyof typeof TECH_OPTIONS, optionId: string, - ): boolean => { + ): string | null => { + if (currentStack.backend === "convex") { + if (category === "runtime" && optionId !== "none") { + return "Convex backend requires runtime to be 'None'. Convex handles its own runtime."; + } + if (category === "database" && optionId !== "none") { + return "Convex backend requires database to be 'None'. Convex provides its own database."; + } + if (category === "orm" && optionId !== "none") { + return "Convex backend requires ORM to be 'None'. Convex has built-in data access."; + } + if (category === "api" && optionId !== "none") { + return "Convex backend requires API to be 'None'. Convex provides its own API layer."; + } + if (category === "dbSetup" && optionId !== "none") { + return "Convex backend requires DB Setup to be 'None'. Convex handles database setup automatically."; + } + if (category === "auth" && optionId === "better-auth") { + return "Convex backend is not compatible with Better-Auth. Use Clerk authentication instead."; + } + } + + if (currentStack.backend === "none") { + if (category === "runtime" && optionId !== "none") { + return "No backend selected: Runtime must be 'None' for frontend-only projects."; + } + if (category === "database" && optionId !== "none") { + return "No backend selected: Database must be 'None' for frontend-only projects."; + } + if (category === "orm" && optionId !== "none") { + return "No backend selected: ORM must be 'None' for frontend-only projects."; + } + if (category === "api" && optionId !== "none") { + return "No backend selected: API must be 'None' for frontend-only projects."; + } + if (category === "auth" && optionId !== "none") { + return "No backend selected: Authentication must be 'None' for frontend-only projects."; + } + if (category === "dbSetup" && optionId !== "none") { + return "No backend selected: DB Setup must be 'None' for frontend-only projects."; + } + if (category === "serverDeploy" && optionId !== "none") { + return "No backend selected: Server deployment must be 'None' for frontend-only projects."; + } + } + const simulatedStack: StackState = JSON.parse(JSON.stringify(currentStack)); const updateArrayCategory = (arr: string[], cat: string): string[] => { @@ -1684,13 +1605,19 @@ const StackBuilder = () => { const isAlchemyServerDeploy = finalStack.serverDeploy === "alchemy"; if (isAlchemyWebDeploy || isAlchemyServerDeploy) { - return false; + return "Next.js is temporarily not compatible with Alchemy deployment. Support coming soon!"; + } + } + + if (category === "webFrontend" && optionId === "solid") { + if (finalStack.backend === "convex") { + return "Solid is not compatible with Convex backend. Try TanStack Router, React Router, or Next.js instead."; } } if (category === "auth" && optionId === "clerk") { if (finalStack.backend !== "convex") { - return false; + return "Clerk authentication only works with Convex backend. Switch to Convex backend to use Clerk."; } const hasClerkCompatibleFrontend = @@ -1707,7 +1634,13 @@ const StackBuilder = () => { ); if (!hasClerkCompatibleFrontend) { - return false; + return "Clerk requires TanStack Router, React Router, TanStack Start, Next.js, or React Native frontend."; + } + } + + if (category === "auth" && optionId === "better-auth") { + if (finalStack.backend === "convex") { + return "Better-Auth is not compatible with Convex backend. Use Clerk authentication instead."; } } @@ -1716,7 +1649,7 @@ const StackBuilder = () => { finalStack.runtime === "workers" && optionId !== "hono" ) { - return false; + return "Cloudflare Workers runtime only supports Hono backend. Switch to Hono to use Workers runtime."; } if ( @@ -1724,7 +1657,15 @@ const StackBuilder = () => { optionId === "workers" && finalStack.backend !== "hono" ) { - return false; + return "Cloudflare Workers runtime requires Hono backend. Switch to Hono backend first."; + } + + if ( + category === "runtime" && + optionId === "none" && + finalStack.backend !== "convex" + ) { + return "Runtime 'None' is only available with Convex backend. Switch to Convex to use this option."; } if ( @@ -1732,7 +1673,7 @@ const StackBuilder = () => { finalStack.database === "none" && optionId !== "none" ) { - return false; + return "ORM requires a database. Select a database first (SQLite, PostgreSQL, MySQL, or MongoDB)."; } if ( @@ -1740,7 +1681,79 @@ const StackBuilder = () => { optionId !== "none" && finalStack.orm === "none" ) { - return false; + return "Database requires an ORM. Select an ORM first (Drizzle, Prisma, or Mongoose)."; + } + + if (category === "database" && optionId === "mongodb") { + if (finalStack.orm !== "prisma" && finalStack.orm !== "mongoose") { + return "MongoDB requires Prisma or Mongoose ORM. Select one of these ORMs first."; + } + } + + if (category === "orm" && optionId === "mongoose") { + if (finalStack.database !== "mongodb") { + return "Mongoose ORM only works with MongoDB database. Select MongoDB first."; + } + } + + if (category === "dbSetup" && optionId === "turso") { + if (finalStack.database !== "sqlite") { + return "Turso requires SQLite database. Select SQLite first."; + } + if (finalStack.orm !== "drizzle") { + return "Turso requires Drizzle ORM. Select Drizzle first."; + } + } + + if (category === "dbSetup" && optionId === "d1") { + if (finalStack.database !== "sqlite") { + return "Cloudflare D1 requires SQLite database. Select SQLite first."; + } + if (finalStack.orm !== "drizzle") { + return "Cloudflare D1 requires Drizzle ORM. Select Drizzle first."; + } + if (finalStack.runtime !== "workers") { + return "Cloudflare D1 requires Cloudflare Workers runtime. Select Workers runtime first."; + } + if (finalStack.backend !== "hono") { + return "Cloudflare D1 requires Hono backend. Select Hono backend first."; + } + } + + if (category === "dbSetup" && optionId === "prisma-postgres") { + if (finalStack.database !== "postgres") { + return "Prisma PostgreSQL setup requires PostgreSQL database. Select PostgreSQL first."; + } + } + + if (category === "dbSetup" && optionId === "mongodb-atlas") { + if (finalStack.database !== "mongodb") { + return "MongoDB Atlas requires MongoDB database. Select MongoDB first."; + } + if (finalStack.orm !== "prisma" && finalStack.orm !== "mongoose") { + return "MongoDB Atlas requires Prisma or Mongoose ORM. Select one of these ORMs first."; + } + } + + if (category === "dbSetup" && optionId === "neon") { + if (finalStack.database !== "postgres") { + return "Neon requires PostgreSQL database. Select PostgreSQL first."; + } + } + + if (category === "dbSetup" && optionId === "supabase") { + if (finalStack.database !== "postgres") { + return "Supabase requires PostgreSQL database. Select PostgreSQL first."; + } + } + + if (category === "dbSetup" && optionId === "docker") { + if (finalStack.database === "sqlite") { + return "Docker setup is not needed for SQLite. SQLite works without Docker."; + } + if (finalStack.runtime === "workers") { + return "Docker setup is not compatible with Cloudflare Workers runtime. Use D1 instead."; + } } if ( @@ -1748,18 +1761,94 @@ const StackBuilder = () => { finalStack.runtime === "workers" && optionId === "none" ) { - return false; + return "Cloudflare Workers runtime requires a server deployment. Select Wrangler or Alchemy."; } if ( - category === "webFrontend" || - category === "nativeFrontend" || - category === "addons" || - category === "examples" + category === "serverDeploy" && + (optionId === "alchemy" || optionId === "wrangler") && + finalStack.runtime !== "workers" ) { - return (finalStack[category] as string[]).includes(optionId); + return `${optionId === "alchemy" ? "Alchemy" : "Wrangler"} deployment requires Cloudflare Workers runtime. Select Workers runtime first.`; } - return finalStack[category] === optionId; + + if ( + category === "serverDeploy" && + (optionId === "alchemy" || optionId === "wrangler") && + finalStack.backend !== "hono" + ) { + return `${optionId === "alchemy" ? "Alchemy" : "Wrangler"} deployment requires Hono backend. Select Hono backend first.`; + } + + if ( + category === "serverDeploy" && + optionId !== "none" && + (finalStack.backend === "none" || finalStack.backend === "convex") + ) { + return "Server deployment requires a supported backend (Hono, Express, Fastify, or Elysia). Convex has its own deployment."; + } + + if (category === "webDeploy" && optionId !== "none") { + const hasWebFrontend = finalStack.webFrontend.some((f) => f !== "none"); + if (!hasWebFrontend) { + return "Web deployment requires a web frontend. Select a web frontend first."; + } + } + + if (category === "api" && optionId === "trpc") { + const isNuxt = finalStack.webFrontend.includes("nuxt"); + const isSvelte = finalStack.webFrontend.includes("svelte"); + const isSolid = finalStack.webFrontend.includes("solid"); + if (isNuxt || isSvelte || isSolid) { + const frontendName = isNuxt ? "Nuxt" : isSvelte ? "Svelte" : "Solid"; + return `${frontendName} requires oRPC API. tRPC is not compatible with ${frontendName}.`; + } + } + + if (category === "addons" && optionId === "pwa") { + const hasPWACompat = hasPWACompatibleFrontend(finalStack.webFrontend); + if (!hasPWACompat) { + return "PWA addon requires TanStack Router, React Router, Solid, or Next.js frontend."; + } + } + + if (category === "addons" && optionId === "tauri") { + const hasTauriCompat = hasTauriCompatibleFrontend(finalStack.webFrontend); + if (!hasTauriCompat) { + return "Tauri addon requires TanStack Router, React Router, Nuxt, Svelte, Solid, or Next.js frontend."; + } + } + + if (category === "addons" && optionId === "ultracite") { + if (finalStack.addons.includes("biome")) { + return "Ultracite already includes Biome configuration. Remove Biome addon first."; + } + } + + if (category === "examples" && optionId === "todo") { + if (finalStack.database === "none") { + return "Todo example requires a database. Select a database first."; + } + } + + if (category === "examples" && optionId === "ai") { + if (finalStack.backend === "elysia") { + return "AI example is not compatible with Elysia backend. Try Hono, Express, or Fastify."; + } + if (finalStack.webFrontend.includes("solid")) { + return "AI example is not compatible with Solid frontend. Try React-based frontends."; + } + } + + return null; + }; + + const isOptionCompatible = ( + currentStack: StackState, + category: keyof typeof TECH_OPTIONS, + optionId: string, + ): boolean => { + return getDisabledReason(currentStack, category, optionId) === null; }; return ( @@ -1767,8 +1856,8 @@ const StackBuilder = () => {
-
-
+
+
-
- - - {lastSavedStack && ( - - )} - - -
- -
+
$ @@ -1887,27 +1928,94 @@ const StackBuilder = () => {
-
-

- Quick Presets -

-
- {PRESET_TEMPLATES.map((preset) => ( +
+
+
- ))} + +
+ +
+ {lastSavedStack ? ( + + ) : ( +
+ )} + +
+ + + + + + + + + + + {PRESET_TEMPLATES.map((preset) => ( + applyPreset(preset.id)} + className="flex flex-col items-start gap-1 p-3" + > +
+ {preset.name} +
+
{preset.description}
+
+ ))} +
+
@@ -1986,6 +2094,14 @@ const StackBuilder = () => { tech.id, ); + const disabledReason = isDisabled + ? getDisabledReason( + stack, + categoryKey as keyof typeof TECH_OPTIONS, + tech.id, + ) + : null; + return ( @@ -2045,6 +2161,15 @@ const StackBuilder = () => { )} + {disabledReason && ( + +

{disabledReason}

+
+ )}
); })} diff --git a/apps/web/src/app/(home)/stack/_components/stack-display.tsx b/apps/web/src/app/(home)/stack/_components/stack-display.tsx new file mode 100644 index 0000000..6fbcce4 --- /dev/null +++ b/apps/web/src/app/(home)/stack/_components/stack-display.tsx @@ -0,0 +1,220 @@ +"use client"; + +import { Check, ChevronDown, Copy, Edit, Share2, Terminal } from "lucide-react"; +import Link from "next/link"; +import { usePathname, useSearchParams } from "next/navigation"; +import { useState } from "react"; +import { toast } from "sonner"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { ShareDialog } from "@/components/ui/share-dialog"; +import { TechBadge } from "@/components/ui/tech-badge"; +import { type StackState, TECH_OPTIONS } from "@/lib/constant"; +import type { LoadedStackState } from "@/lib/stack-server"; +import { + CATEGORY_ORDER, + generateStackSummary, + generateStackUrl, +} from "@/lib/stack-utils"; +import { cn } from "@/lib/utils"; +import PackageIcon from "../../_components/icons"; + +interface StackDisplayProps { + stackState: LoadedStackState; +} + +export function StackDisplay({ stackState }: StackDisplayProps) { + const pathname = usePathname(); + const searchParamsHook = useSearchParams(); + const [copied, setCopied] = useState(false); + const [selectedPM, setSelectedPM] = useState<"npm" | "pnpm" | "bun">("bun"); + + const stackUrl = generateStackUrl(pathname, searchParamsHook); + const stack = stackState; + const stackSummary = generateStackSummary(stack); + + const commands = { + npm: "npx create-better-t-stack@latest", + pnpm: "pnpm create better-t-stack@latest", + bun: "bun create better-t-stack@latest", + }; + + const command = commands[selectedPM]; + + const techBadges = (() => { + const badges: React.ReactNode[] = []; + for (const category of CATEGORY_ORDER) { + const categoryKey = category as keyof StackState; + const options = TECH_OPTIONS[category as keyof typeof TECH_OPTIONS]; + const selectedValue = stack[categoryKey]; + + if (!options) continue; + + if (Array.isArray(selectedValue)) { + if ( + selectedValue.length === 0 || + (selectedValue.length === 1 && selectedValue[0] === "none") + ) { + continue; + } + + for (const id of selectedValue) { + if (id === "none") continue; + const tech = options.find((opt) => opt.id === id); + if (tech) { + badges.push( + , + ); + } + } + } else { + const tech = options.find((opt) => opt.id === selectedValue); + if ( + !tech || + tech.id === "none" || + tech.id === "false" || + ((category === "git" || + category === "install" || + category === "auth") && + tech.id === "true") + ) { + continue; + } + badges.push( + , + ); + } + } + return badges; + })(); + + const copyCommand = async () => { + try { + await navigator.clipboard.writeText(command); + setCopied(true); + toast.success("Command copied to clipboard!"); + setTimeout(() => setCopied(false), 2000); + } catch { + toast.error("Failed to copy command"); + } + }; + + return ( +
+
+
+
+
+

Tech Stack

+

{stackSummary}

+
+ +
+ + + + + + + +
+
+
+ +
+
+
+
+ + GENERATE_COMMAND +
+ + + + + + {(["bun", "pnpm", "npm"] as const).map((pm) => ( + setSelectedPM(pm)} + className={cn( + "flex items-center gap-2", + selectedPM === pm && "bg-accent text-background", + )} + > + + {pm.toUpperCase()} + {selectedPM === pm && ( + + )} + + ))} + + +
+ +
+
+ $ + {command} +
+ +
+
+
+ +
+

+ Technologies +

+
+ {techBadges.length > 0 ? ( + techBadges + ) : ( +

No technologies selected

+ )} +
+
+
+
+ ); +} diff --git a/apps/web/src/app/(home)/stack/page.tsx b/apps/web/src/app/(home)/stack/page.tsx new file mode 100644 index 0000000..51f784e --- /dev/null +++ b/apps/web/src/app/(home)/stack/page.tsx @@ -0,0 +1,42 @@ +import type { Metadata } from "next"; +import { Suspense } from "react"; +import { loadStackParams } from "@/lib/stack-server"; +import { StackDisplay } from "./_components/stack-display"; + +interface StackPageProps { + searchParams: Promise<{ [key: string]: string | string[] | undefined }>; +} + +export const metadata: Metadata = { + title: "Tech Stack - Better-T-Stack", + description: "View and share your custom tech stack configuration", + openGraph: { + title: "Tech Stack - Better-T-Stack", + description: "View and share your custom tech stack configuration", + url: "https://better-t-stack.dev/stack", + images: [ + { + url: "https://r2.better-t-stack.dev/og.png", + width: 1200, + height: 630, + alt: "Better-T-Stack Tech Stack", + }, + ], + }, + twitter: { + card: "summary_large_image", + title: "Tech Stack - Better-T-Stack", + description: "View and share your custom tech stack configuration", + images: ["https://r2.better-t-stack.dev/og.png"], + }, +}; + +export default async function StackPage({ searchParams }: StackPageProps) { + const stackState = await loadStackParams(searchParams); + + return ( + + + + ); +} diff --git a/apps/web/src/components/ai/page-actions.tsx b/apps/web/src/components/ai/page-actions.tsx index 7ef08fd..451981b 100644 --- a/apps/web/src/components/ai/page-actions.tsx +++ b/apps/web/src/components/ai/page-actions.tsx @@ -15,7 +15,7 @@ import { MessageCircleIcon, } from "lucide-react"; import { useMemo, useState } from "react"; -import { cn } from "../../../lib/cn"; +import { cn } from "@/lib/utils"; const cache = new Map(); diff --git a/apps/web/src/components/ui/kibo-ui/qr-code/index.tsx b/apps/web/src/components/ui/kibo-ui/qr-code/index.tsx new file mode 100644 index 0000000..e4712b9 --- /dev/null +++ b/apps/web/src/components/ui/kibo-ui/qr-code/index.tsx @@ -0,0 +1,88 @@ +"use client"; + +import { formatHex, oklch } from "culori"; +import QR from "qrcode"; +import { type HTMLAttributes, useEffect, useState } from "react"; +import { cn } from "@/lib/utils"; + +export type QRCodeProps = HTMLAttributes & { + data: string; + foreground?: string; + background?: string; + robustness?: "L" | "M" | "Q" | "H"; +}; + +const oklchRegex = /oklch\(([0-9.]+)\s+([0-9.]+)\s+([0-9.]+)\)/; + +const getOklch = (color: string, fallback: [number, number, number]) => { + const oklchMatch = color.match(oklchRegex); + + if (!oklchMatch) { + return { l: fallback[0], c: fallback[1], h: fallback[2] }; + } + + return { + l: Number.parseFloat(oklchMatch[1]), + c: Number.parseFloat(oklchMatch[2]), + h: Number.parseFloat(oklchMatch[3]), + }; +}; + +export const QRCode = ({ + data, + foreground, + background, + robustness = "M", + className, + ...props +}: QRCodeProps) => { + const [svg, setSVG] = useState(null); + + useEffect(() => { + const generateQR = async () => { + try { + const styles = getComputedStyle(document.documentElement); + const foregroundColor = + foreground ?? styles.getPropertyValue("--foreground"); + const backgroundColor = + background ?? styles.getPropertyValue("--background"); + + const foregroundOklch = getOklch( + foregroundColor, + [0.21, 0.006, 285.885], + ); + const backgroundOklch = getOklch(backgroundColor, [0.985, 0, 0]); + + const newSvg = await QR.toString(data, { + type: "svg", + color: { + dark: formatHex(oklch({ mode: "oklch", ...foregroundOklch })), + light: formatHex(oklch({ mode: "oklch", ...backgroundOklch })), + }, + width: 200, + errorCorrectionLevel: robustness, + margin: 0, + }); + + setSVG(newSvg); + } catch (err) { + console.error(err); + } + }; + + generateQR(); + }, [data, foreground, background, robustness]); + + if (!svg) { + return null; + } + + return ( +
+ ); +}; diff --git a/apps/web/src/components/ui/kibo-ui/qr-code/server.tsx b/apps/web/src/components/ui/kibo-ui/qr-code/server.tsx new file mode 100644 index 0000000..73f7258 --- /dev/null +++ b/apps/web/src/components/ui/kibo-ui/qr-code/server.tsx @@ -0,0 +1,42 @@ +import QR from "qrcode"; +import type { HTMLAttributes } from "react"; +import { cn } from "@/lib/utils"; + +export type QRCodeProps = HTMLAttributes & { + 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, { + type: "svg", + color: { + dark: foreground, + light: background, + }, + width: 200, + errorCorrectionLevel: robustness, + }); + + if (!svg) { + throw new Error("Failed to generate QR code"); + } + + return ( +
+ ); +}; diff --git a/apps/web/src/components/ui/share-dialog.tsx b/apps/web/src/components/ui/share-dialog.tsx new file mode 100644 index 0000000..fe7706b --- /dev/null +++ b/apps/web/src/components/ui/share-dialog.tsx @@ -0,0 +1,188 @@ +"use client"; + +import { Check, Copy, Share2, Twitter } from "lucide-react"; +import { useState } from "react"; +import { toast } from "sonner"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { QRCode } from "@/components/ui/kibo-ui/qr-code"; +import { TechBadge } from "@/components/ui/tech-badge"; +import type { StackState } from "@/lib/constant"; +import { TECH_OPTIONS } from "@/lib/constant"; +import { CATEGORY_ORDER } from "@/lib/stack-utils"; +import { cn } from "@/lib/utils"; + +interface ShareDialogProps { + children: React.ReactNode; + stackUrl: string; + stackState: StackState; +} + +export function ShareDialog({ + children, + stackUrl, + stackState, +}: ShareDialogProps) { + const [copied, setCopied] = useState(false); + + const techBadges = (() => { + const badges: React.ReactNode[] = []; + for (const category of CATEGORY_ORDER) { + const categoryKey = category as keyof StackState; + const options = TECH_OPTIONS[category as keyof typeof TECH_OPTIONS]; + const selectedValue = stackState[categoryKey]; + + if (!options) continue; + + if (Array.isArray(selectedValue)) { + if ( + selectedValue.length === 0 || + (selectedValue.length === 1 && selectedValue[0] === "none") + ) { + continue; + } + + for (const id of selectedValue) { + if (id === "none") continue; + const tech = options.find((opt) => opt.id === id); + if (tech) { + badges.push( + , + ); + } + } + } else { + const tech = options.find((opt) => opt.id === selectedValue); + if ( + !tech || + tech.id === "none" || + tech.id === "false" || + ((category === "git" || + category === "install" || + category === "auth") && + tech.id === "true") + ) { + continue; + } + badges.push( + , + ); + } + } + return badges; + })(); + + 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"); + } + }; + + const shareToTwitter = () => { + const text = encodeURIComponent( + `Check out this cool tech stack I configured with Create Better T Stack!\n\nšŸš€ ${techBadges.length} technologies selected\n\n`, + ); + const url = encodeURIComponent(stackUrl); + window.open( + `https://twitter.com/intent/tweet?text=${text}&url=${url}`, + "_blank", + ); + }; + + return ( + + {children} + + + + + Share Your Stack + + + Share your custom tech stack configuration with others + + + +
+
+
+ Technologies +
+
+ {techBadges.length > 0 ? ( + techBadges + ) : ( + + No technologies selected + + )} +
+
+ +
+
QR Code
+
+
+ +
+
+

+ Scan to view this tech stack +

+
+ +
+
Share
+
+ + +
+
+
+
+
+ ); +} diff --git a/apps/web/src/components/ui/tech-badge.tsx b/apps/web/src/components/ui/tech-badge.tsx new file mode 100644 index 0000000..ca3a8ab --- /dev/null +++ b/apps/web/src/components/ui/tech-badge.tsx @@ -0,0 +1,109 @@ +"use client"; + +import Image from "next/image"; +import { useTheme } from "next-themes"; +import { cn } from "@/lib/utils"; + +interface TechBadgeProps { + icon: string; + name: string; + category: string; + className?: string; +} + +const getBadgeColors = (category: string): string => { + switch (category) { + case "webFrontend": + case "nativeFrontend": + return "border-blue-300 bg-blue-100 text-blue-800 dark:border-blue-700/30 dark:bg-blue-900/30 dark:text-blue-300"; + case "runtime": + return "border-amber-300 bg-amber-100 text-amber-800 dark:border-amber-700/30 dark:bg-amber-900/30 dark:text-amber-300"; + case "backend": + return "border-sky-300 bg-sky-100 text-sky-800 dark:border-sky-700/30 dark:bg-sky-900/30 dark:text-sky-300"; + case "api": + return "border-indigo-300 bg-indigo-100 text-indigo-800 dark:border-indigo-700/30 dark:bg-indigo-900/30 dark:text-indigo-300"; + case "database": + return "border-emerald-300 bg-emerald-100 text-emerald-800 dark:border-emerald-700/30 dark:bg-emerald-900/30 dark:text-emerald-300"; + case "orm": + return "border-cyan-300 bg-cyan-100 text-cyan-800 dark:border-cyan-700/30 dark:bg-cyan-900/30 dark:text-cyan-300"; + case "auth": + return "border-green-300 bg-green-100 text-green-800 dark:border-green-700/30 dark:bg-green-900/30 dark:text-green-300"; + case "dbSetup": + return "border-pink-300 bg-pink-100 text-pink-800 dark:border-pink-700/30 dark:bg-pink-900/30 dark:text-pink-300"; + case "addons": + return "border-violet-300 bg-violet-100 text-violet-800 dark:border-violet-700/30 dark:bg-violet-900/30 dark:text-violet-300"; + case "examples": + return "border-teal-300 bg-teal-100 text-teal-800 dark:border-teal-700/30 dark:bg-teal-900/30 dark:text-teal-300"; + case "packageManager": + return "border-orange-300 bg-orange-100 text-orange-800 dark:border-orange-700/30 dark:bg-orange-900/30 dark:text-orange-300"; + case "git": + case "webDeploy": + case "serverDeploy": + case "install": + return "border-gray-300 bg-gray-100 text-gray-700 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400"; + default: + return "border-gray-300 bg-gray-100 text-gray-800 dark:border-gray-700/30 dark:bg-gray-900/30 dark:text-gray-300"; + } +}; + +function TechIcon({ + icon, + name, + className, +}: { + icon: string; + name: string; + className?: string; +}) { + const { theme } = useTheme(); + + if (!icon) return null; + + if (!icon.startsWith("https://")) { + return ( + + {icon} + + ); + } + + let iconSrc = icon; + if ( + theme === "light" && + (icon.includes("drizzle") || + icon.includes("prisma") || + icon.includes("express") || + icon.includes("clerk")) + ) { + iconSrc = icon.replace(".svg", "-light.svg"); + } + + return ( + {`${name} + ); +} + +export function TechBadge({ icon, name, category, className }: TechBadgeProps) { + return ( + + {icon !== "" && ( + + )} + {name} + + ); +} diff --git a/apps/web/src/lib/stack-server.ts b/apps/web/src/lib/stack-server.ts new file mode 100644 index 0000000..8445385 --- /dev/null +++ b/apps/web/src/lib/stack-server.ts @@ -0,0 +1,65 @@ +import { + createLoader, + parseAsArrayOf, + parseAsString, + parseAsStringEnum, +} from "nuqs/server"; +import { DEFAULT_STACK, type StackState, TECH_OPTIONS } from "@/lib/constant"; + +const getValidIds = (category: keyof typeof TECH_OPTIONS): string[] => { + return TECH_OPTIONS[category]?.map((opt) => opt.id) ?? []; +}; + +// Server-side parsers (same as client-side but imported from nuqs/server) +const serverStackParsers = { + projectName: parseAsString.withDefault(DEFAULT_STACK.projectName), + webFrontend: parseAsArrayOf(parseAsString).withDefault( + DEFAULT_STACK.webFrontend, + ), + nativeFrontend: parseAsArrayOf(parseAsString).withDefault( + DEFAULT_STACK.nativeFrontend, + ), + runtime: parseAsStringEnum( + getValidIds("runtime"), + ).withDefault(DEFAULT_STACK.runtime), + backend: parseAsStringEnum( + getValidIds("backend"), + ).withDefault(DEFAULT_STACK.backend), + api: parseAsStringEnum(getValidIds("api")).withDefault( + DEFAULT_STACK.api, + ), + database: parseAsStringEnum( + getValidIds("database"), + ).withDefault(DEFAULT_STACK.database), + orm: parseAsStringEnum(getValidIds("orm")).withDefault( + DEFAULT_STACK.orm, + ), + dbSetup: parseAsStringEnum( + getValidIds("dbSetup"), + ).withDefault(DEFAULT_STACK.dbSetup), + auth: parseAsStringEnum(getValidIds("auth")).withDefault( + DEFAULT_STACK.auth, + ), + packageManager: parseAsStringEnum( + getValidIds("packageManager"), + ).withDefault(DEFAULT_STACK.packageManager), + addons: parseAsArrayOf(parseAsString).withDefault(DEFAULT_STACK.addons), + examples: parseAsArrayOf(parseAsString).withDefault(DEFAULT_STACK.examples), + git: parseAsStringEnum(["true", "false"]).withDefault( + DEFAULT_STACK.git, + ), + install: parseAsStringEnum([ + "true", + "false", + ]).withDefault(DEFAULT_STACK.install), + webDeploy: parseAsStringEnum( + getValidIds("webDeploy"), + ).withDefault(DEFAULT_STACK.webDeploy), + serverDeploy: parseAsStringEnum( + getValidIds("serverDeploy"), + ).withDefault(DEFAULT_STACK.serverDeploy), +}; + +export const loadStackParams = createLoader(serverStackParsers); + +export type LoadedStackState = Awaited>; diff --git a/apps/web/src/lib/stack-url-state.ts b/apps/web/src/lib/stack-url-state.ts index 37eaa4b..1d11ac8 100644 --- a/apps/web/src/lib/stack-url-state.ts +++ b/apps/web/src/lib/stack-url-state.ts @@ -83,4 +83,5 @@ export const stackQueryStatesOptions = { history: "replace" as const, shallow: false, urlKeys: stackUrlKeys, + clearOnDefault: true, }; diff --git a/apps/web/src/lib/stack-utils.ts b/apps/web/src/lib/stack-utils.ts new file mode 100644 index 0000000..be15efd --- /dev/null +++ b/apps/web/src/lib/stack-utils.ts @@ -0,0 +1,467 @@ +import { + parseAsArrayOf, + parseAsString, + parseAsStringEnum, + useQueryState, + useQueryStates, +} from "nuqs"; +import { + DEFAULT_STACK, + isStackDefault, + type StackState, + TECH_OPTIONS, +} from "@/lib/constant"; +import { + stackParsers, + stackQueryStatesOptions, + stackUrlKeys, +} from "@/lib/stack-url-state"; + +const getValidIds = (category: keyof typeof TECH_OPTIONS): string[] => { + return TECH_OPTIONS[category]?.map((opt) => opt.id) ?? []; +}; + +const CATEGORY_ORDER: Array = [ + "webFrontend", + "nativeFrontend", + "backend", + "runtime", + "api", + "database", + "orm", + "dbSetup", + "webDeploy", + "serverDeploy", + "auth", + "packageManager", + "addons", + "examples", + "git", + "install", +]; + +function getStackKeyFromUrlKey(urlKey: string): keyof StackState | null { + for (const [stackKey, urlKeyValue] of Object.entries(stackUrlKeys)) { + if (urlKeyValue === urlKey) { + return stackKey as keyof StackState; + } + } + return null; +} + +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)[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)[key] = defaultValue; + } + } + + return parsedStack; +} + +/** + * Generate a human-readable summary of the stack + */ +export function generateStackSummary(stack: StackState): string { + const selectedTechs: string[] = []; + + for (const category of CATEGORY_ORDER) { + const categoryKey = category as keyof StackState; + const options = TECH_OPTIONS[category as keyof typeof TECH_OPTIONS]; + const selectedValue = stack[categoryKey]; + + if (!options) continue; + + if (Array.isArray(selectedValue)) { + if ( + selectedValue.length === 0 || + (selectedValue.length === 1 && selectedValue[0] === "none") + ) { + continue; + } + + for (const id of selectedValue) { + if (id === "none") continue; + const tech = options.find((opt) => opt.id === id); + if (tech) { + selectedTechs.push(tech.name); + } + } + } else { + const tech = options.find((opt) => opt.id === selectedValue); + if ( + !tech || + tech.id === "none" || + tech.id === "false" || + ((category === "git" || + category === "install" || + category === "auth") && + tech.id === "true") + ) { + continue; + } + selectedTechs.push(tech.name); + } + } + + return selectedTechs.length > 0 ? selectedTechs.join(" • ") : "Custom stack"; +} + +export function generateStackCommand(stack: StackState): string { + let base: string; + switch (stack.packageManager) { + case "npm": + base = "npx create-better-t-stack@latest"; + break; + case "pnpm": + base = "pnpm create better-t-stack@latest"; + break; + default: + base = "bun create better-t-stack@latest"; + break; + } + + const projectName = stack.projectName || "my-better-t-app"; + const flags: string[] = []; + + const isDefaultStack = Object.keys(DEFAULT_STACK).every((key) => { + if (key === "projectName") return true; + const defaultKey = key as keyof StackState; + return isStackDefault(stack, defaultKey, stack[defaultKey]); + }); + + if (isDefaultStack) { + flags.push("--yes"); + } else { + const combinedFrontends = [ + ...stack.webFrontend, + ...stack.nativeFrontend, + ].filter((v, _, arr) => v !== "none" || arr.length === 1); + + if (combinedFrontends.length === 0 || combinedFrontends[0] === "none") { + flags.push("--frontend none"); + } else { + flags.push(`--frontend ${combinedFrontends.join(" ")}`); + } + + flags.push(`--backend ${stack.backend}`); + flags.push(`--runtime ${stack.runtime}`); + flags.push(`--api ${stack.api}`); + flags.push(`--auth ${stack.auth}`); + flags.push(`--database ${stack.database}`); + flags.push(`--orm ${stack.orm}`); + flags.push(`--db-setup ${stack.dbSetup}`); + flags.push(`--package-manager ${stack.packageManager}`); + + if (stack.git === "false") { + flags.push("--no-git"); + } else { + flags.push("--git"); + } + + flags.push(`--web-deploy ${stack.webDeploy}`); + flags.push(`--server-deploy ${stack.serverDeploy}`); + + if (stack.install === "false") { + flags.push("--no-install"); + } else { + flags.push("--install"); + } + + if (stack.addons.length > 0) { + const validAddons = stack.addons.filter((addon) => + [ + "pwa", + "tauri", + "starlight", + "biome", + "husky", + "turborepo", + "ultracite", + "fumadocs", + "oxlint", + "ruler", + ].includes(addon), + ); + if (validAddons.length > 0) { + flags.push(`--addons ${validAddons.join(" ")}`); + } + } else { + flags.push("--addons none"); + } + + if (stack.examples.length > 0) { + flags.push(`--examples ${stack.examples.join(" ")}`); + } else { + flags.push("--examples none"); + } + } + + return `${base} ${projectName}${ + flags.length > 0 ? ` ${flags.join(" ")}` : "" + }`; +} + +/** + * Generate stack URL from pathname and search params + */ +export function generateStackUrl( + pathname: string, + searchParams: URLSearchParams, +): string { + const searchString = searchParams.toString(); + const relativeUrl = `${pathname}${searchString ? `?${searchString}` : ""}`; + return `https://better-t-stack.dev${relativeUrl}`; +} + +export function generateStackUrlFromState( + stack: StackState, + baseUrl?: string, +): string { + const origin = + baseUrl || + (typeof window !== "undefined" + ? window.location.origin + : "https://better-t-stack.dev"); + + const isDefaultStack = Object.keys(DEFAULT_STACK).every((key) => { + if (key === "projectName") return true; + const defaultKey = key as keyof StackState; + return isStackDefault(stack, defaultKey, stack[defaultKey]); + }); + + if (isDefaultStack) { + return `${origin}/stack`; + } + + const stackParams = new URLSearchParams(); + + for (const [stackKey, urlKey] of Object.entries(stackUrlKeys)) { + const value = stack[stackKey as keyof StackState]; + if (value !== undefined) { + if (Array.isArray(value)) { + stackParams.set(urlKey, value.join(",")); + } else { + stackParams.set(urlKey, String(value)); + } + } + } + + return `${origin}/stack?${stackParams.toString()}`; +} + +export function useStackStateWithAllParams() { + const [stack, setStack] = useQueryStates( + stackParsers, + stackQueryStatesOptions, + ); + + const setStackWithAllParams = async ( + newStack: Partial | ((prev: StackState) => Partial), + ) => { + const updatedStack = + typeof newStack === "function" ? newStack(stack) : newStack; + const finalStack = { ...stack, ...updatedStack }; + + const isFinalStackDefault = Object.keys(DEFAULT_STACK).every((key) => { + if (key === "projectName") return true; + const defaultKey = key as keyof StackState; + return isStackDefault(finalStack, defaultKey, finalStack[defaultKey]); + }); + + if (isFinalStackDefault) { + await setStack(null); + } else { + await setStack(finalStack); + } + }; + + return [stack, setStackWithAllParams] as const; +} + +export function useIndividualStackStates() { + const [projectName, setProjectName] = useQueryState( + "name", + parseAsString.withDefault(DEFAULT_STACK.projectName), + ); + + const [webFrontend, setWebFrontend] = useQueryState( + "fe-w", + parseAsArrayOf(parseAsString).withDefault(DEFAULT_STACK.webFrontend), + ); + + const [nativeFrontend, setNativeFrontend] = useQueryState( + "fe-n", + parseAsArrayOf(parseAsString).withDefault(DEFAULT_STACK.nativeFrontend), + ); + + const [runtime, setRuntime] = useQueryState( + "rt", + parseAsStringEnum(getValidIds("runtime")).withDefault( + DEFAULT_STACK.runtime, + ), + ); + + const [backend, setBackend] = useQueryState( + "be", + parseAsStringEnum(getValidIds("backend")).withDefault( + DEFAULT_STACK.backend, + ), + ); + + const [api, setApi] = useQueryState( + "api", + parseAsStringEnum(getValidIds("api")).withDefault(DEFAULT_STACK.api), + ); + + const [database, setDatabase] = useQueryState( + "db", + parseAsStringEnum(getValidIds("database")).withDefault( + DEFAULT_STACK.database, + ), + ); + + const [orm, setOrm] = useQueryState( + "orm", + parseAsStringEnum(getValidIds("orm")).withDefault(DEFAULT_STACK.orm), + ); + + const [dbSetup, setDbSetup] = useQueryState( + "dbs", + parseAsStringEnum(getValidIds("dbSetup")).withDefault( + DEFAULT_STACK.dbSetup, + ), + ); + + const [auth, setAuth] = useQueryState( + "au", + parseAsStringEnum(getValidIds("auth")).withDefault(DEFAULT_STACK.auth), + ); + + const [packageManager, setPackageManager] = useQueryState( + "pm", + parseAsStringEnum(getValidIds("packageManager")).withDefault( + DEFAULT_STACK.packageManager, + ), + ); + + const [addons, setAddons] = useQueryState( + "add", + parseAsArrayOf(parseAsString).withDefault(DEFAULT_STACK.addons), + ); + + const [examples, setExamples] = useQueryState( + "ex", + parseAsArrayOf(parseAsString).withDefault(DEFAULT_STACK.examples), + ); + + const [git, setGit] = useQueryState( + "git", + parseAsStringEnum(["true", "false"] as const).withDefault( + DEFAULT_STACK.git as "true" | "false", + ), + ); + + const [install, setInstall] = useQueryState( + "i", + parseAsStringEnum(["true", "false"] as const).withDefault( + DEFAULT_STACK.install as "true" | "false", + ), + ); + + const [webDeploy, setWebDeploy] = useQueryState( + "wd", + parseAsStringEnum(getValidIds("webDeploy")).withDefault( + DEFAULT_STACK.webDeploy, + ), + ); + + const [serverDeploy, setServerDeploy] = useQueryState( + "sd", + parseAsStringEnum(getValidIds("serverDeploy")).withDefault( + DEFAULT_STACK.serverDeploy, + ), + ); + + const stack: StackState = { + projectName, + webFrontend, + nativeFrontend, + runtime, + backend, + api, + database, + orm, + dbSetup, + auth, + packageManager, + addons, + examples, + git, + install, + webDeploy, + serverDeploy, + }; + + const setStack = async (updates: Partial) => { + const setters = { + projectName: setProjectName, + webFrontend: setWebFrontend, + nativeFrontend: setNativeFrontend, + runtime: setRuntime, + backend: setBackend, + api: setApi, + database: setDatabase, + orm: setOrm, + dbSetup: setDbSetup, + auth: setAuth, + packageManager: setPackageManager, + addons: setAddons, + examples: setExamples, + git: setGit, + install: setInstall, + webDeploy: setWebDeploy, + serverDeploy: setServerDeploy, + }; + + const promises = Object.entries(updates).map(([key, value]) => { + const setter = setters[key as keyof typeof setters]; + return setter(value as never); + }); + + await Promise.all(promises); + }; + + return [stack, setStack] as const; +} + +export { CATEGORY_ORDER }; diff --git a/bun.lock b/bun.lock index 93e1dc7..86e644b 100644 --- a/bun.lock +++ b/bun.lock @@ -60,6 +60,7 @@ "clsx": "^2.1.1", "convex": "^1.25.4", "convex-helpers": "^0.1.104", + "culori": "^4.0.2", "date-fns": "^4.1.0", "fumadocs-core": "15.6.7", "fumadocs-mdx": "11.7.3", @@ -71,6 +72,7 @@ "nuqs": "^2.4.3", "papaparse": "^5.5.3", "posthog-js": "^1.258.5", + "qrcode": "^1.5.4", "radix-ui": "^1.4.2", "react": "^19.1.1", "react-dom": "^19.1.1", @@ -85,9 +87,11 @@ }, "devDependencies": { "@tailwindcss/postcss": "^4.1.11", + "@types/culori": "^4.0.0", "@types/mdx": "^2.0.13", "@types/node": "24.1.0", "@types/papaparse": "^5.3.16", + "@types/qrcode": "^1.5.5", "@types/react": "^19.1.9", "@types/react-dom": "^19.1.7", "eslint": "^9.32.0", @@ -947,6 +951,8 @@ "@types/chai": ["@types/chai@5.2.2", "", { "dependencies": { "@types/deep-eql": "*" } }, "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg=="], + "@types/culori": ["@types/culori@4.0.0", "", {}, "sha512-aFljQwjb++sl6TAyEXeHTiK/fk9epZOQ+nMmadjnAvzZFIvNoQ0x8XQYfcOaRTBwmDUPUlghhZCJ66MTcqQAsg=="], + "@types/d3-array": ["@types/d3-array@3.2.1", "", {}, "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg=="], "@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="], @@ -997,6 +1003,8 @@ "@types/papaparse": ["@types/papaparse@5.3.16", "", { "dependencies": { "@types/node": "*" } }, "sha512-T3VuKMC2H0lgsjI9buTB3uuKj3EMD2eap1MOuEQuBQ44EnDx/IkGhU6EwiTf9zG3za4SKlmwKAImdDKdNnCsXg=="], + "@types/qrcode": ["@types/qrcode@1.5.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg=="], + "@types/react": ["@types/react@19.1.12", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w=="], "@types/react-dom": ["@types/react-dom@19.1.9", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ=="], @@ -1203,6 +1211,8 @@ "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + "camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="], + "caniuse-lite": ["caniuse-lite@1.0.30001737", "", {}, "sha512-BiloLiXtQNrY5UyF0+1nSJLXUENuhka2pzy2Fx5pGxqavdrxSCW4U6Pn/PoG3Efspi2frRbHpBV2XsrPE6EDlw=="], "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], @@ -1309,6 +1319,8 @@ "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + "culori": ["culori@4.0.2", "", {}, "sha512-1+BhOB8ahCn4O0cep0Sh2l9KCOfOdY+BXJnKMHFFzDEouSr/el18QwXEMRlOj9UY5nCeA8UN3a/82rUWRBeyBw=="], + "d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="], "d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="], @@ -1343,6 +1355,8 @@ "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], + "decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="], + "decimal.js-light": ["decimal.js-light@2.5.1", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="], "decode-named-character-reference": ["decode-named-character-reference@1.2.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q=="], @@ -1379,6 +1393,8 @@ "diff": ["diff@8.0.2", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="], + "dijkstrajs": ["dijkstrajs@1.0.3", "", {}, "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA=="], + "doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="], "dom-helpers": ["dom-helpers@5.2.1", "", { "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" } }, "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA=="], @@ -1735,7 +1751,7 @@ "is-finalizationregistry": ["is-finalizationregistry@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg=="], - "is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="], + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], "is-generator-function": ["is-generator-function@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "get-proto": "^1.0.0", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ=="], @@ -2131,6 +2147,8 @@ "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], + "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], "paged-request": ["paged-request@2.0.2", "", { "dependencies": { "axios": "^0.21.1" } }, "sha512-NWrGqneZImDdcMU/7vMcAOo1bIi5h/pmpJqe7/jdsy85BA/s5MSaU/KlpxwW/IVPmIwBcq2uKPrBWWhEWhtxag=="], @@ -2177,6 +2195,8 @@ "pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="], + "pngjs": ["pngjs@5.0.0", "", {}, "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="], + "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], @@ -2203,6 +2223,8 @@ "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + "qrcode": ["qrcode@1.5.4", "", { "dependencies": { "dijkstrajs": "^1.0.1", "pngjs": "^5.0.0", "yargs": "^15.3.1" }, "bin": { "qrcode": "bin/qrcode" } }, "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg=="], + "qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="], "quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="], @@ -2277,6 +2299,10 @@ "remeda": ["remeda@2.30.0", "", { "dependencies": { "type-fest": "^4.41.0" } }, "sha512-TcRpI1ecqnMer3jHhFtMerGvHFCDlCHljUp0/9A4HxHOh5bSY3kP1l8nQDFMnWYJKl3MSarDNY1tb0Bs/bCmvw=="], + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + + "require-main-filename": ["require-main-filename@2.0.0", "", {}, "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="], + "resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="], "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], @@ -2323,6 +2349,8 @@ "serve-static": ["serve-static@2.2.0", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ=="], + "set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="], + "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], "set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="], @@ -2619,6 +2647,8 @@ "which-collection": ["which-collection@1.0.2", "", { "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", "is-weakmap": "^2.0.2", "is-weakset": "^2.0.3" } }, "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw=="], + "which-module": ["which-module@2.0.1", "", {}, "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="], + "which-typed-array": ["which-typed-array@1.1.19", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw=="], "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], @@ -3361,20 +3391,22 @@ "prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + "qrcode/yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="], + "restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], "router/path-to-regexp": ["path-to-regexp@8.2.0", "", {}, "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ=="], "slice-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], + "slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="], + "string-width/emoji-regex": ["emoji-regex@10.5.0", "", {}, "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg=="], "string-width/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - "string-width-cjs/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], "to-object-path/kind-of": ["kind-of@3.2.2", "", { "dependencies": { "is-buffer": "^1.1.5" } }, "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ=="], @@ -3973,6 +4005,16 @@ "p-locate/p-limit/yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + "qrcode/yargs/cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="], + + "qrcode/yargs/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], + + "qrcode/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "qrcode/yargs/y18n": ["y18n@4.0.3", "", {}, "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="], + + "qrcode/yargs/yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="], + "string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.0", "", {}, "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg=="], "vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.9", "", { "os": "aix", "cpu": "ppc64" }, "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA=="], @@ -4029,8 +4071,6 @@ "wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - "wrap-ansi-cjs/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.2.0", "", {}, "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg=="], "@aws-sdk/client-dynamodb/@aws-crypto/sha256-js/@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], @@ -4179,6 +4219,12 @@ "changelogen/c12/giget/tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="], + "qrcode/yargs/cliui/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], + + "qrcode/yargs/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], + + "qrcode/yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + "@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-endpoint/@smithy/url-parser/@smithy/querystring-parser": ["@smithy/querystring-parser@4.0.5", "", { "dependencies": { "@smithy/types": "^4.3.2", "tslib": "^2.6.2" } }, "sha512-6SV7md2CzNG/WUeTjVe6Dj8noH32r4MnUeFKZrnVYsQxpGSIcphAanQMayi8jJLZAWm6pdM9ZXvKCpWOsIGg0w=="], "@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream/@smithy/fetch-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.0.5", "", { "dependencies": { "@smithy/types": "^4.3.2", "@smithy/util-uri-escape": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-NJeSCU57piZ56c+/wY+AbAw6rxCCAOZLCIniRE7wqvndqxcKKDOXzwWjrY7wGKEISfhL9gBbAaWWgHsUGedk+A=="], @@ -4227,6 +4273,8 @@ "changelogen/c12/giget/tar/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + "qrcode/yargs/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], + "@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream/@smithy/fetch-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg=="], "@aws-sdk/core/@smithy/smithy-client/@smithy/util-stream/@smithy/node-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg=="], @@ -4236,5 +4284,7 @@ "@aws-sdk/middleware-sdk-sqs/@smithy/smithy-client/@smithy/util-stream/@smithy/node-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg=="], "changelogen/c12/giget/tar/minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "qrcode/yargs/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], } }