diff --git a/.changeset/scaffold-shipping-ux.md b/.changeset/scaffold-shipping-ux.md new file mode 100644 index 00000000..b2dfe7d3 --- /dev/null +++ b/.changeset/scaffold-shipping-ux.md @@ -0,0 +1,17 @@ +--- +'@tanstack/cli': minor +'@tanstack/create': minor +--- + +feat(cli, create): close the gap between `tanstack create` and shipping a real app + +A bundle of UX improvements aimed at beginners (especially those coming from Next.js) and the AI agents they pair with: + +- **Tailored post-creation next steps.** The scaffold completion message now lists the env vars you still need to fill in `.env.local`, links the docs for each shipping-critical integration you picked (auth, database, ORM, deployment), and surfaces the Intent-wired AGENTS.md / CLAUDE.md with concrete prompt examples. +- **Pre-creation review screen.** After interactive prompts, the CLI shows a categorized summary (auth, database, ORM, deploy, other) and asks for confirmation before writing files. Conflicting selections (two auth providers, two ORMs, etc.) are flagged in the same step. +- **`.env.example` generation.** A checked-in `.env.example` is now derived from the env-var schemas of selected add-ons, with descriptions and a `(required)` marker. Plays nicely with add-ons that ship their own `_dot_env.example.append`. +- **Better add-on descriptions.** Concept-first one-liners replace generic "Add X to your application." Reads like a menu instead of a list of brand names. +- **Deployment quickstarts.** Each `--deployment` host (Netlify, Cloudflare, Railway, Nitro) now contributes its own README section explaining the actual steps to ship — push, dashboard URL, env var sync. +- **Clerk demo route parity.** Clerk's scaffold now ships a proper sign-in flow (matching Better Auth's depth) using Clerk's prebuilt components, plus a richer README with route-protection patterns and a production checklist. +- **Intent install passes `--map`.** The auto-invoked `intent install` now writes explicit task→skill mappings into the agent config instead of relying on runtime discovery, so agents see directly which skill matches which task. +- **`tanstack clean-demos` command.** A new subcommand removes leftover `demo.*` and `example.*` files (and prunes empty `routes/demo`/`routes/example` directories) so a beginner can ship without the scaffold's training wheels. diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index 164a1f5f..2a96e36c 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -1,5 +1,5 @@ import fs from 'node:fs' -import { resolve } from 'node:path' +import { relative, resolve } from 'node:path' import { Command, InvalidArgumentError, Option } from 'commander' import { cancel, confirm, intro, isCancel, log } from '@clack/prompts' import chalk from 'chalk' @@ -17,6 +17,7 @@ import { getFrameworks, initAddOn, initStarter, + isDemoFilePath, } from '@tanstack/create' import { LIBRARY_GROUPS, @@ -290,6 +291,159 @@ export function cli({ } } + async function confirmCreateOptions(finalOptions: Options) { + const lines: Array = [] + lines.push(` Project: ${finalOptions.projectName}`) + lines.push(` Location: ${finalOptions.targetDir}`) + lines.push(` Framework: ${finalOptions.framework.name}`) + lines.push(` Mode: ${finalOptions.mode}`) + lines.push(` Package manager: ${finalOptions.packageManager}`) + if (finalOptions.starter) { + lines.push(` Template: ${finalOptions.starter.name}`) + } + + const auth: Array = [] + const database: Array = [] + const orm: Array = [] + const deploy: Array = [] + const otherAddOns: Array = [] + for (const addOn of finalOptions.chosenAddOns) { + switch (addOn.category) { + case 'auth': + auth.push(addOn.name) + break + case 'database': + database.push(addOn.name) + break + case 'orm': + orm.push(addOn.name) + break + case 'deploy': + deploy.push(addOn.name) + break + default: + otherAddOns.push(addOn.name) + } + } + + if ( + auth.length + + database.length + + orm.length + + deploy.length + + otherAddOns.length > + 0 + ) { + lines.push('') + } + if (auth.length > 0) { + lines.push(` Auth: ${auth.join(', ')}`) + } + if (database.length > 0) { + lines.push(` Database: ${database.join(', ')}`) + } + if (orm.length > 0) { + lines.push(` ORM: ${orm.join(', ')}`) + } + if (deploy.length > 0) { + lines.push(` Deploy: ${deploy.join(', ')}`) + } + if (otherAddOns.length > 0) { + lines.push(` Other add-ons: ${otherAddOns.join(', ')}`) + } + + lines.push('') + lines.push(` Initialize git: ${finalOptions.git ? 'yes' : 'no'}`) + lines.push( + ` Install deps: ${finalOptions.install === false ? 'no' : 'yes'}`, + ) + lines.push(` Agent skills: ${finalOptions.intent ? 'yes' : 'no'}`) + + log.info(`About to create:\n\n${lines.join('\n')}`) + + const conflicts = findExclusiveConflicts(finalOptions.chosenAddOns) + if (conflicts.length > 0) { + log.warn( + `Conflicting selections detected:\n${conflicts + .map((c) => ` • ${c.category}: ${c.names.join(', ')}`) + .join('\n')}`, + ) + } + + const shouldContinue = await confirm({ + message: 'Continue with these settings?', + initialValue: true, + }) + + if (isCancel(shouldContinue) || !shouldContinue) { + cancel('Operation cancelled.') + process.exit(0) + } + } + + const CLEAN_DEMOS_SKIP_DIRS = new Set([ + 'node_modules', + '.git', + 'dist', + '.output', + '.tanstack', + '.nitro', + '.wrangler', + ]) + + function findDemoFiles(root: string): Array { + const results: Array = [] + function walk(dir: string) { + let entries: Array + try { + entries = fs.readdirSync(dir, { withFileTypes: true }) + } catch { + return + } + for (const entry of entries) { + const full = resolve(dir, entry.name) + if (entry.isDirectory()) { + if (CLEAN_DEMOS_SKIP_DIRS.has(entry.name)) continue + walk(full) + } else if (entry.isFile() && isDemoFilePath(full)) { + results.push(full) + } + } + } + walk(root) + return results.sort() + } + + function pruneEmptyDemoDirs(root: string) { + const candidates = ['src/routes/demo', 'src/routes/example'] + for (const rel of candidates) { + const dir = resolve(root, rel) + if (!fs.existsSync(dir)) continue + try { + if (fs.readdirSync(dir).length === 0) { + fs.rmdirSync(dir) + } + } catch { + // ignore + } + } + } + + function findExclusiveConflicts( + addOns: Options['chosenAddOns'], + ): Array<{ category: string; names: Array }> { + const buckets: Record> = {} + for (const addOn of addOns) { + for (const exclusive of addOn.exclusive || []) { + buckets[exclusive] ??= [] + buckets[exclusive].push(addOn.name) + } + } + return Object.entries(buckets) + .filter(([_, names]) => names.length > 1) + .map(([category, names]) => ({ category, names })) + } + const availableFrameworks = getFrameworks().map((f) => f.name) function resolveBuiltInDevWatchPath(frameworkId: string): string { @@ -694,6 +848,7 @@ export function cli({ ) } + let cameFromPrompts = false if (finalOptions) { intro(`Creating a new ${appName} app in ${projectName}...`) } else { @@ -711,6 +866,7 @@ export function cli({ ? getFrameworkByName(defaultFramework)?.id : undefined, }) + cameFromPrompts = true } if (!finalOptions) { @@ -734,6 +890,9 @@ export function cli({ finalOptions.targetDir = resolve(process.cwd(), finalOptions.projectName) } + if (cameFromPrompts) { + await confirmCreateOptions(finalOptions) + } await confirmTargetDirectorySafety(finalOptions.targetDir, options.force) await createApp(environment, finalOptions) }, @@ -1388,6 +1547,87 @@ Remove your node_modules directory and package lock file and re-install.`, } }) + // === CLEAN-DEMOS SUBCOMMAND === + program + .command('clean-demos') + .description('Remove demo/example files from a scaffolded TanStack project') + .argument('[target-dir]', 'project directory (default: current directory)', '.') + .addOption( + new Option(AGENT_FLAG, 'internal: invocation originated from an agent').hideHelp(), + ) + .option('-y, --yes', 'skip confirmation prompt', false) + .option('--dry-run', 'list files without deleting', false) + .action( + async ( + targetDir: string, + cmdOptions: { yes: boolean; dryRun: boolean }, + ) => { + try { + await runWithTelemetry( + 'clean-demos', + { + properties: { + yes: cmdOptions.yes, + dry_run: cmdOptions.dryRun, + }, + }, + async (telemetry) => { + const root = resolve(targetDir) + if (!fs.existsSync(root)) { + throw new Error(`Directory not found: ${root}`) + } + if (!fs.existsSync(resolve(root, '.cta.json'))) { + log.warn( + `No .cta.json in ${root} — this may not be a TanStack scaffold. Continuing anyway.`, + ) + } + + const demoFiles = findDemoFiles(root) + telemetry.mergeProperties({ result_count: demoFiles.length }) + + if (demoFiles.length === 0) { + log.info('No demo or example files found.') + return + } + + log.info( + `Found ${demoFiles.length} demo/example file(s):\n${demoFiles + .map((f) => ` • ${relative(root, f)}`) + .join('\n')}`, + ) + + if (cmdOptions.dryRun) { + log.info('(dry run — nothing deleted)') + return + } + + if (!cmdOptions.yes) { + const ok = await confirm({ + message: 'Delete these files?', + initialValue: false, + }) + if (isCancel(ok) || !ok) { + cancel('Operation cancelled.') + process.exit(0) + } + } + + for (const file of demoFiles) { + fs.rmSync(file, { force: true }) + } + pruneEmptyDemoDirs(root) + log.info( + `Deleted ${demoFiles.length} file(s). Run your dev server to regenerate routeTree.gen.ts.`, + ) + }, + ) + } catch (error) { + log.error(formatErrorMessage(error)) + process.exit(1) + } + }, + ) + const telemetryCommand = program.command('telemetry') telemetryCommand .command('status') diff --git a/packages/create/src/create-app.ts b/packages/create/src/create-app.ts index a0cc4d50..f2371c9b 100644 --- a/packages/create/src/create-app.ts +++ b/packages/create/src/create-app.ts @@ -1,6 +1,6 @@ import { basename, resolve } from 'node:path' -import { isBase64 } from './file-helpers.js' +import { isBase64, isDemoFilePath } from './file-helpers.js' import { formatCommand } from './utils.js' import { writeConfigFileToEnvironment } from './config-file.js' import { @@ -18,26 +18,6 @@ import { runSpecialSteps } from './special-steps/index.js' import type { Environment, FileBundleHandler, Options } from './types.js' -function isDemoFilePath(path?: string) { - if (!path) return false - const normalized = path.replace(/\\/g, '/') - - if ( - normalized.includes('/routes/demo/') || - normalized.includes('/routes/example/') - ) { - return true - } - - const filename = normalized.split('/').pop() || '' - return ( - filename.startsWith('demo.') || - filename.startsWith('demo-') || - filename.startsWith('example.') || - filename.startsWith('example-') - ) -} - function stripExamplesFromOptions(options: Options): Options { if (options.includeExamples !== false) { return options @@ -325,6 +305,87 @@ async function seedEnvValues(environment: Environment, options: Options) { await environment.writeFile(envLocalPath, envContents) } +async function writeEnvExample(environment: Environment, options: Options) { + const envExamplePath = resolve(options.targetDir, '.env.example') + const existing = environment.exists(envExamplePath) + ? await environment.readFile(envExamplePath) + : '' + + const declared = new Set() + for (const match of existing.matchAll(/^([A-Z_][A-Z0-9_]*)=/gm)) { + declared.add(match[1]) + } + + const sections: Array = [] + for (const addOn of options.chosenAddOns) { + const lines: Array = [] + for (const envVar of addOn.envVars || []) { + if (declared.has(envVar.name)) continue + declared.add(envVar.name) + if (envVar.description) { + const required = envVar.required ? ' (required)' : '' + lines.push(`# ${envVar.description}${required}`) + } + lines.push(`${envVar.name}=`) + } + if (lines.length > 0) { + sections.push(`# ${addOn.name}\n${lines.join('\n')}`) + } + } + + if (sections.length === 0) return + + const additions = sections.join('\n\n') + const newContent = existing + ? `${existing.trimEnd()}\n\n${additions}\n` + : `${additions}\n` + + await environment.writeFile(envExamplePath, newContent) +} + +const SHIPPING_CATEGORIES = new Set(['auth', 'database', 'orm', 'deploy']) + +function buildNextSteps(options: Options): string { + const collectedEnv = new Set(Object.keys(options.envVarValues || {})) + const listedEnvVars = new Set() + const envVarLines: Array = [] + const docLines: Array = [] + + for (const addOn of options.chosenAddOns) { + if (addOn.link && addOn.category && SHIPPING_CATEGORIES.has(addOn.category)) { + docLines.push(` • ${addOn.name} (${addOn.category}) — ${addOn.link}`) + } + + for (const envVar of addOn.envVars || []) { + if (listedEnvVars.has(envVar.name)) continue + listedEnvVars.add(envVar.name) + const required = envVar.required ? ' (required)' : '' + const status = collectedEnv.has(envVar.name) + ? ' — already set from your input' + : ' — needs a value' + const desc = envVar.description ? ` — ${envVar.description}` : '' + envVarLines.push(` • ${envVar.name}${required}${desc}${status}`) + } + } + + const sections: Array = [] + if (envVarLines.length > 0) { + sections.push( + `Environment variables (review/fill in .env.local before deploying):\n${envVarLines.join('\n')}`, + ) + } + if (docLines.length > 0) { + sections.push(`Docs for the integrations you picked:\n${docLines.join('\n')}`) + } + if (options.intent) { + sections.push( + `Working with an AI agent? Your agent config (AGENTS.md / CLAUDE.md) was wired up by TanStack Intent\nwith explicit skill mappings for the libraries you installed. Try asking your agent:\n • "migrate this Next.js page to TanStack Start"\n • "add a protected /dashboard route"\n • "show me how to use TanStack Router search params"`, + ) + } + + return sections.length > 0 ? `\nNext steps:\n\n${sections.join('\n\n')}\n` : '' +} + function report(environment: Environment, options: Options) { const warnings: Array = [] for (const addOn of options.chosenAddOns) { @@ -358,6 +419,8 @@ ${environment.getErrors().join('\n')}` : `% cd ${options.projectName} ` + const nextSteps = buildNextSteps(options) + // Use the force luke! :) environment.outro( `${locationMessage} @@ -366,7 +429,7 @@ Use the following commands to start your app: ${cdInstruction}% ${formatCommand( getPackageManagerScriptCommand(options.packageManager, ['dev']), )} - +${nextSteps} Please read the README.md file for information on testing, styling, adding routes, etc.${errorStatement}`, ) } @@ -377,6 +440,7 @@ export async function createApp(environment: Environment, options: Options) { environment.startRun() await writeFiles(environment, effectiveOptions) await seedEnvValues(environment, effectiveOptions) + await writeEnvExample(environment, effectiveOptions) await runCommandsAndInstallDependencies(environment, effectiveOptions) environment.finishRun() diff --git a/packages/create/src/file-helpers.ts b/packages/create/src/file-helpers.ts index 8dd7b780..38149bf9 100644 --- a/packages/create/src/file-helpers.ts +++ b/packages/create/src/file-helpers.ts @@ -112,6 +112,26 @@ export function isDirectory(path: string): boolean { return statSync(path).isDirectory() } +export function isDemoFilePath(path?: string): boolean { + if (!path) return false + const normalized = path.replace(/\\/g, '/') + + if ( + normalized.includes('/routes/demo/') || + normalized.includes('/routes/example/') + ) { + return true + } + + const filename = normalized.split('/').pop() || '' + return ( + filename.startsWith('demo.') || + filename.startsWith('demo-') || + filename.startsWith('example.') || + filename.startsWith('example-') + ) +} + export function findFilesRecursively( path: string, files: Record, diff --git a/packages/create/src/frameworks/react/add-ons/ai/info.json b/packages/create/src/frameworks/react/add-ons/ai/info.json index 55da3ba2..7d8d6edb 100644 --- a/packages/create/src/frameworks/react/add-ons/ai/info.json +++ b/packages/create/src/frameworks/react/add-ons/ai/info.json @@ -1,6 +1,6 @@ { "name": "AI", - "description": "TanStack AI integration and examples.", + "description": "Streaming chat UI with model-agnostic backend (OpenAI, Anthropic, etc).", "phase": "add-on", "modes": ["file-router"], "type": "add-on", diff --git a/packages/create/src/frameworks/react/add-ons/better-auth/info.json b/packages/create/src/frameworks/react/add-ons/better-auth/info.json index d93971b3..4a02bcf4 100644 --- a/packages/create/src/frameworks/react/add-ons/better-auth/info.json +++ b/packages/create/src/frameworks/react/add-ons/better-auth/info.json @@ -1,6 +1,6 @@ { "name": "Better Auth", - "description": "Add Better Auth authentication to your application.", + "description": "Self-hosted user accounts and sessions (open source, full control).", "phase": "add-on", "type": "add-on", "category": "auth", diff --git a/packages/create/src/frameworks/react/add-ons/clerk/README.md b/packages/create/src/frameworks/react/add-ons/clerk/README.md index be48584a..0a9b45ec 100644 --- a/packages/create/src/frameworks/react/add-ons/clerk/README.md +++ b/packages/create/src/frameworks/react/add-ons/clerk/README.md @@ -1,3 +1,44 @@ ## Setting up Clerk -- Set the `VITE_CLERK_PUBLISHABLE_KEY` in your `.env.local`. +1. Sign up at [clerk.com](https://clerk.com) and create an application +2. Copy the **Publishable Key** from the Clerk dashboard +3. Set it in your `.env.local`: + ```bash + VITE_CLERK_PUBLISHABLE_KEY=pk_test_... + ``` +4. Visit the demo route at `/demo/clerk` once `npm run dev` is running + +### What's wired up + +- **``** at the app root (`src/integrations/clerk/provider.tsx`) handles auth context for the whole tree +- **`` / ``** in the header swap based on auth state +- **`/demo/clerk`** shows Clerk's prebuilt sign-in UI and a signed-in greeting + +### Protecting a route + +Wrap any component in `` / ``: + +```tsx +import { SignedIn, SignedOut, RedirectToSignIn } from '@clerk/clerk-react' + +function ProtectedPage() { + return ( + <> + + + + + + + + ) +} +``` + +For server-side checks (route loaders, server functions), see the Clerk docs on [`auth()`](https://clerk.com/docs/references/backend/auth). + +### Production checklist + +- Replace the test keys with **production keys** from a dedicated production Clerk instance +- Configure your production domain under **Domains** in the Clerk dashboard +- Set up social providers (Google, GitHub, etc.) under **User & Authentication → Social Connections** diff --git a/packages/create/src/frameworks/react/add-ons/clerk/assets/src/routes/demo/clerk.tsx b/packages/create/src/frameworks/react/add-ons/clerk/assets/src/routes/demo/clerk.tsx index 14676584..5bc51c63 100644 --- a/packages/create/src/frameworks/react/add-ons/clerk/assets/src/routes/demo/clerk.tsx +++ b/packages/create/src/frameworks/react/add-ons/clerk/assets/src/routes/demo/clerk.tsx @@ -1,20 +1,103 @@ import { createFileRoute } from '@tanstack/react-router' -import { useUser } from '@clerk/clerk-react' +import { + SignIn, + SignedIn, + SignedOut, + useUser, +} from '@clerk/clerk-react' export const Route = createFileRoute('/demo/clerk')({ - component: App, + component: ClerkDemo, }) -function App() { - const { isSignedIn, user, isLoaded } = useUser() +function ClerkDemo() { + return ( +
+
+ +
+

+ Sign in to continue +

+

+ Clerk renders the sign-in UI, manages sessions, and handles social providers for you. +

+
+
+ +
+

+ Built with{' '} + + CLERK + + . +

+
- if (!isLoaded) { - return
Loading...
- } + + + +
+
+ ) +} + +function SignedInGreeting() { + const { user } = useUser() + if (!user) return null + + const email = user.primaryEmailAddress?.emailAddress + const initial = (user.firstName || email || 'U').charAt(0).toUpperCase() + + return ( +
+
+

+ Welcome back +

+

+ You're signed in as {email} +

+
- if (!isSignedIn) { - return
Sign in to view this page
- } +
+ {user.imageUrl ? ( + + ) : ( +
+ + {initial} + +
+ )} +
+

+ {user.firstName} {user.lastName} +

+

+ {email} +

+
+
- return
Hello {user.firstName}!
+

+ Manage your account from the avatar in the header. Built with{' '} + + CLERK + + . +

+
+ ) } diff --git a/packages/create/src/frameworks/react/add-ons/clerk/info.json b/packages/create/src/frameworks/react/add-ons/clerk/info.json index 3d1143bb..f3d8f41a 100644 --- a/packages/create/src/frameworks/react/add-ons/clerk/info.json +++ b/packages/create/src/frameworks/react/add-ons/clerk/info.json @@ -1,6 +1,6 @@ { "name": "Clerk", - "description": "Add Clerk authentication to your application.", + "description": "Hosted user accounts with prebuilt sign-in UI and social providers (managed).", "phase": "add-on", "modes": ["file-router"], "type": "add-on", diff --git a/packages/create/src/frameworks/react/add-ons/compiler/info.json b/packages/create/src/frameworks/react/add-ons/compiler/info.json index 0d59fd53..f9107656 100644 --- a/packages/create/src/frameworks/react/add-ons/compiler/info.json +++ b/packages/create/src/frameworks/react/add-ons/compiler/info.json @@ -1,7 +1,7 @@ { "name": "Compiler", "phase": "setup", - "description": "Add React Compiler", + "description": "Auto-memoize components and hooks (fewer re-renders, no manual useMemo/useCallback).", "link": "https://react.dev/learn/react-compiler", "modes": ["code-router", "file-router"], "type": "add-on", diff --git a/packages/create/src/frameworks/react/add-ons/convex/info.json b/packages/create/src/frameworks/react/add-ons/convex/info.json index 18491b2e..7a3da454 100644 --- a/packages/create/src/frameworks/react/add-ons/convex/info.json +++ b/packages/create/src/frameworks/react/add-ons/convex/info.json @@ -1,6 +1,6 @@ { "name": "Convex", - "description": "Add the Convex database to your application.", + "description": "Reactive document database with real-time queries and serverless functions.", "link": "https://convex.dev", "phase": "add-on", "type": "add-on", diff --git a/packages/create/src/frameworks/react/add-ons/drizzle/info.json b/packages/create/src/frameworks/react/add-ons/drizzle/info.json index 60f51f70..983885a5 100644 --- a/packages/create/src/frameworks/react/add-ons/drizzle/info.json +++ b/packages/create/src/frameworks/react/add-ons/drizzle/info.json @@ -1,6 +1,6 @@ { "name": "Drizzle", - "description": "Add Drizzle ORM to your application.", + "description": "Type-safe SQL query builder for Postgres, SQLite, or MySQL.", "phase": "add-on", "type": "add-on", "category": "orm", diff --git a/packages/create/src/frameworks/react/add-ons/mcp/info.json b/packages/create/src/frameworks/react/add-ons/mcp/info.json index b9bb3ca2..9ab8f42b 100644 --- a/packages/create/src/frameworks/react/add-ons/mcp/info.json +++ b/packages/create/src/frameworks/react/add-ons/mcp/info.json @@ -1,7 +1,7 @@ { "name": "MCP", "phase": "setup", - "description": "Add Model Context Protocol (MCP) support.", + "description": "Expose your app as an MCP server so AI clients (Claude, Cursor) can call into it.", "link": "https://mcp.dev", "modes": ["file-router"], "type": "add-on", diff --git a/packages/create/src/frameworks/react/add-ons/neon/info.json b/packages/create/src/frameworks/react/add-ons/neon/info.json index a1f1d644..5d2cd96d 100644 --- a/packages/create/src/frameworks/react/add-ons/neon/info.json +++ b/packages/create/src/frameworks/react/add-ons/neon/info.json @@ -1,6 +1,6 @@ { "name": "Neon", - "description": "Add the Neon database to your application.", + "description": "Serverless Postgres (auto-scales to zero, branchable per environment).", "link": "https://neon.com", "phase": "add-on", "type": "add-on", diff --git a/packages/create/src/frameworks/react/add-ons/paraglide/info.json b/packages/create/src/frameworks/react/add-ons/paraglide/info.json index b7d7ff4f..d9a231ef 100644 --- a/packages/create/src/frameworks/react/add-ons/paraglide/info.json +++ b/packages/create/src/frameworks/react/add-ons/paraglide/info.json @@ -1,6 +1,6 @@ { "name": "Paraglide (i18n)", - "description": "i18n with localized routing", + "description": "Type-safe i18n with localized routing and message bundles.", "phase": "add-on", "modes": ["file-router"], "type": "add-on", diff --git a/packages/create/src/frameworks/react/add-ons/prisma/info.json b/packages/create/src/frameworks/react/add-ons/prisma/info.json index 2db494ca..8e495a6d 100644 --- a/packages/create/src/frameworks/react/add-ons/prisma/info.json +++ b/packages/create/src/frameworks/react/add-ons/prisma/info.json @@ -1,6 +1,6 @@ { "name": "Prisma", - "description": "Add Prisma Postgres, or Prisma ORM with other DBs to your application.", + "description": "Type-safe database client with schema migrations (Postgres, MySQL, SQLite, MongoDB).", "phase": "add-on", "type": "add-on", "category": "orm", diff --git a/packages/create/src/frameworks/react/add-ons/shadcn/info.json b/packages/create/src/frameworks/react/add-ons/shadcn/info.json index 72e922ca..958b2d2b 100644 --- a/packages/create/src/frameworks/react/add-ons/shadcn/info.json +++ b/packages/create/src/frameworks/react/add-ons/shadcn/info.json @@ -1,6 +1,6 @@ { "name": "Shadcn", - "description": "Add Shadcn UI to your application.", + "description": "Copy-paste accessible UI components (Tailwind + Radix primitives).", "phase": "add-on", "modes": ["file-router", "code-router"], "link": "https://ui.shadcn.com/", diff --git a/packages/create/src/frameworks/react/add-ons/strapi/info.json b/packages/create/src/frameworks/react/add-ons/strapi/info.json index e7840573..42bd6e88 100644 --- a/packages/create/src/frameworks/react/add-ons/strapi/info.json +++ b/packages/create/src/frameworks/react/add-ons/strapi/info.json @@ -1,6 +1,6 @@ { "name": "Strapi", - "description": "Use the Strapi CMS to manage your content.", + "description": "Headless CMS with admin UI (self-hosted, content models in TypeScript).", "link": "https://strapi.io/", "phase": "add-on", "type": "add-on", diff --git a/packages/create/src/frameworks/react/add-ons/t3env/info.json b/packages/create/src/frameworks/react/add-ons/t3env/info.json index 83ce2290..55a3b856 100644 --- a/packages/create/src/frameworks/react/add-ons/t3env/info.json +++ b/packages/create/src/frameworks/react/add-ons/t3env/info.json @@ -1,6 +1,6 @@ { "name": "T3Env", - "description": "Add type safety to your environment variables", + "description": "Validate process.env at build time (catch missing/wrong env vars before runtime).", "phase": "add-on", "type": "add-on", "category": "tooling", diff --git a/packages/create/src/frameworks/react/add-ons/workos/info.json b/packages/create/src/frameworks/react/add-ons/workos/info.json index e2da998f..f051f7ee 100644 --- a/packages/create/src/frameworks/react/add-ons/workos/info.json +++ b/packages/create/src/frameworks/react/add-ons/workos/info.json @@ -1,6 +1,6 @@ { "name": "WorkOS", - "description": "Add WorkOS authentication to your application.", + "description": "Enterprise SSO, SAML, and directory sync (good for B2B apps).", "phase": "add-on", "modes": ["file-router"], "type": "add-on", diff --git a/packages/create/src/frameworks/react/hosts/cloudflare/README.md b/packages/create/src/frameworks/react/hosts/cloudflare/README.md new file mode 100644 index 00000000..12554547 --- /dev/null +++ b/packages/create/src/frameworks/react/hosts/cloudflare/README.md @@ -0,0 +1,11 @@ +## Deploy to Cloudflare Workers + +This project uses the Cloudflare Vite plugin (configured in `vite.config.ts`) and `wrangler.jsonc`: + +1. Install Wrangler: `npm install -g wrangler` +2. Authenticate: `wrangler login` +3. Deploy: `npx wrangler deploy` + +For production env vars, run `wrangler secret put MY_VAR` for each secret listed in `.env.example`. Public (non-secret) vars go in `wrangler.jsonc` under `vars`. + +KV, D1, R2, and Durable Object bindings are configured in `wrangler.jsonc` — see https://developers.cloudflare.com/workers/wrangler/configuration/. diff --git a/packages/create/src/frameworks/react/hosts/cloudflare/info.json b/packages/create/src/frameworks/react/hosts/cloudflare/info.json index fc3e5621..fcd27332 100644 --- a/packages/create/src/frameworks/react/hosts/cloudflare/info.json +++ b/packages/create/src/frameworks/react/hosts/cloudflare/info.json @@ -1,6 +1,6 @@ { "name": "Cloudflare", - "description": "Cloudflare deployment setup", + "description": "Deploy to Cloudflare Workers (edge runtime, KV/D1/R2 bindings).", "link": "https://developers.cloudflare.com/workers/vite-plugin/", "phase": "add-on", "modes": ["file-router", "code-router"], diff --git a/packages/create/src/frameworks/react/hosts/netlify/README.md b/packages/create/src/frameworks/react/hosts/netlify/README.md new file mode 100644 index 00000000..a00ad80f --- /dev/null +++ b/packages/create/src/frameworks/react/hosts/netlify/README.md @@ -0,0 +1,11 @@ +## Deploy to Netlify + +This project ships with `netlify.toml` configured for a Netlify site: + +1. Push this repo to GitHub +2. Visit https://app.netlify.com/start and import the repo +3. Netlify auto-detects the build (`vite build` → `dist/client`) +4. Open **Site settings → Environment variables** and add anything from `.env.example` that needs a real value in production +5. Trigger the first deploy + +Server functions and API routes run on Netlify Functions. For lower-latency request handling, see Netlify Edge Functions: https://docs.netlify.com/edge-functions/overview. diff --git a/packages/create/src/frameworks/react/hosts/netlify/info.json b/packages/create/src/frameworks/react/hosts/netlify/info.json index f0d67092..8da171fd 100644 --- a/packages/create/src/frameworks/react/hosts/netlify/info.json +++ b/packages/create/src/frameworks/react/hosts/netlify/info.json @@ -1,6 +1,6 @@ { "name": "Netlify", - "description": "Netlify deployment setup", + "description": "Deploy to Netlify (Functions and Edge Functions, GitHub-driven).", "link": "https://docs.netlify.com", "phase": "add-on", "modes": ["file-router", "code-router"], diff --git a/packages/create/src/frameworks/react/hosts/nitro/README.md b/packages/create/src/frameworks/react/hosts/nitro/README.md new file mode 100644 index 00000000..027037df --- /dev/null +++ b/packages/create/src/frameworks/react/hosts/nitro/README.md @@ -0,0 +1,12 @@ +## Deploy with Nitro + +This project uses Nitro as a generic server adapter, so it can run on any Node-compatible host. + +```bash +npm run build +node dist/server/index.mjs +``` + +The build output is a self-contained Node server. To deploy, push the `dist/` directory to your host (Render, Fly.io, your own VPS, etc.) and run the server command above. + +For host-specific presets (Vercel, Netlify, Cloudflare, AWS Lambda, etc.) and tuning, see https://v3.nitro.build/deploy. diff --git a/packages/create/src/frameworks/react/hosts/nitro/info.json b/packages/create/src/frameworks/react/hosts/nitro/info.json index 4d85cdf4..685cf4e9 100644 --- a/packages/create/src/frameworks/react/hosts/nitro/info.json +++ b/packages/create/src/frameworks/react/hosts/nitro/info.json @@ -1,6 +1,6 @@ { "name": "Nitro (agnostic)", - "description": "Nitro deployment setup", + "description": "Generic Nitro adapter (deploy to any Node-compatible host).", "link": "https://v3.nitro.build/", "phase": "add-on", "modes": ["file-router", "code-router"], diff --git a/packages/create/src/frameworks/react/hosts/railway/README.md b/packages/create/src/frameworks/react/hosts/railway/README.md new file mode 100644 index 00000000..d60c1f46 --- /dev/null +++ b/packages/create/src/frameworks/react/hosts/railway/README.md @@ -0,0 +1,10 @@ +## Deploy to Railway + +This project ships with `nixpacks.toml` so Railway detects the build automatically: + +1. Push this repo to GitHub +2. Visit https://railway.com/new and create a project from your repo +3. In the **Variables** tab, add the entries from `.env.example` with their production values +4. Railway runs `vite build` and serves from `dist/client` + +Need a database? Click **+ New** in your project to provision Postgres, MySQL, or Redis directly into the same environment — the connection string is auto-injected as `DATABASE_URL`. diff --git a/packages/create/src/frameworks/react/hosts/railway/info.json b/packages/create/src/frameworks/react/hosts/railway/info.json index 3a1ff709..5eeaff0a 100644 --- a/packages/create/src/frameworks/react/hosts/railway/info.json +++ b/packages/create/src/frameworks/react/hosts/railway/info.json @@ -1,6 +1,6 @@ { "name": "Railway", - "description": "Railway deployment setup", + "description": "Deploy to Railway (full-stack PaaS for Node + databases).", "link": "https://railway.com/", "phase": "add-on", "modes": ["file-router", "code-router"], diff --git a/packages/create/src/frameworks/solid/add-ons/better-auth/info.json b/packages/create/src/frameworks/solid/add-ons/better-auth/info.json index 8e47292b..c6f9db43 100644 --- a/packages/create/src/frameworks/solid/add-ons/better-auth/info.json +++ b/packages/create/src/frameworks/solid/add-ons/better-auth/info.json @@ -1,6 +1,6 @@ { "name": "Better Auth", - "description": "Add Better Auth authentication to your application.", + "description": "Self-hosted user accounts and sessions (open source, full control).", "phase": "add-on", "type": "add-on", "category": "auth", diff --git a/packages/create/src/frameworks/solid/add-ons/convex/info.json b/packages/create/src/frameworks/solid/add-ons/convex/info.json index 50c3a089..ff1c558a 100644 --- a/packages/create/src/frameworks/solid/add-ons/convex/info.json +++ b/packages/create/src/frameworks/solid/add-ons/convex/info.json @@ -1,6 +1,6 @@ { "name": "Convex", - "description": "Add the Convex database to your application.", + "description": "Reactive document database with real-time queries and serverless functions.", "link": "https://convex.dev", "phase": "add-on", "type": "add-on", diff --git a/packages/create/src/frameworks/solid/add-ons/solid-ui/info.json b/packages/create/src/frameworks/solid/add-ons/solid-ui/info.json index d2171f0b..242b5b58 100644 --- a/packages/create/src/frameworks/solid/add-ons/solid-ui/info.json +++ b/packages/create/src/frameworks/solid/add-ons/solid-ui/info.json @@ -1,6 +1,6 @@ { "name": "Solid-UI", - "description": "Add Solid-UI to your application.", + "description": "Copy-paste accessible UI components for Solid (Tailwind + Radix-style primitives).", "phase": "add-on", "link": "https://ui.shadcn.com/", "modes": ["file-router", "code-router"], diff --git a/packages/create/src/frameworks/solid/add-ons/strapi/info.json b/packages/create/src/frameworks/solid/add-ons/strapi/info.json index 86c85c2e..dae5a9c1 100644 --- a/packages/create/src/frameworks/solid/add-ons/strapi/info.json +++ b/packages/create/src/frameworks/solid/add-ons/strapi/info.json @@ -1,6 +1,6 @@ { "name": "Strapi", - "description": "Use the Strapi CMS to manage your content.", + "description": "Headless CMS with admin UI (self-hosted, content models in TypeScript).", "link": "https://strapi.io/", "phase": "add-on", "type": "add-on", diff --git a/packages/create/src/frameworks/solid/add-ons/t3env/info.json b/packages/create/src/frameworks/solid/add-ons/t3env/info.json index e2b9a018..17124866 100644 --- a/packages/create/src/frameworks/solid/add-ons/t3env/info.json +++ b/packages/create/src/frameworks/solid/add-ons/t3env/info.json @@ -1,6 +1,6 @@ { "name": "T3Env", - "description": "Add type safety to your environment variables", + "description": "Validate process.env at build time (catch missing/wrong env vars before runtime).", "phase": "add-on", "link": "https://github.com/t3-oss/t3-env", "type": "add-on", diff --git a/packages/create/src/frameworks/solid/hosts/cloudflare/README.md b/packages/create/src/frameworks/solid/hosts/cloudflare/README.md new file mode 100644 index 00000000..12554547 --- /dev/null +++ b/packages/create/src/frameworks/solid/hosts/cloudflare/README.md @@ -0,0 +1,11 @@ +## Deploy to Cloudflare Workers + +This project uses the Cloudflare Vite plugin (configured in `vite.config.ts`) and `wrangler.jsonc`: + +1. Install Wrangler: `npm install -g wrangler` +2. Authenticate: `wrangler login` +3. Deploy: `npx wrangler deploy` + +For production env vars, run `wrangler secret put MY_VAR` for each secret listed in `.env.example`. Public (non-secret) vars go in `wrangler.jsonc` under `vars`. + +KV, D1, R2, and Durable Object bindings are configured in `wrangler.jsonc` — see https://developers.cloudflare.com/workers/wrangler/configuration/. diff --git a/packages/create/src/frameworks/solid/hosts/cloudflare/info.json b/packages/create/src/frameworks/solid/hosts/cloudflare/info.json index fc3e5621..fcd27332 100644 --- a/packages/create/src/frameworks/solid/hosts/cloudflare/info.json +++ b/packages/create/src/frameworks/solid/hosts/cloudflare/info.json @@ -1,6 +1,6 @@ { "name": "Cloudflare", - "description": "Cloudflare deployment setup", + "description": "Deploy to Cloudflare Workers (edge runtime, KV/D1/R2 bindings).", "link": "https://developers.cloudflare.com/workers/vite-plugin/", "phase": "add-on", "modes": ["file-router", "code-router"], diff --git a/packages/create/src/frameworks/solid/hosts/netlify/README.md b/packages/create/src/frameworks/solid/hosts/netlify/README.md new file mode 100644 index 00000000..a00ad80f --- /dev/null +++ b/packages/create/src/frameworks/solid/hosts/netlify/README.md @@ -0,0 +1,11 @@ +## Deploy to Netlify + +This project ships with `netlify.toml` configured for a Netlify site: + +1. Push this repo to GitHub +2. Visit https://app.netlify.com/start and import the repo +3. Netlify auto-detects the build (`vite build` → `dist/client`) +4. Open **Site settings → Environment variables** and add anything from `.env.example` that needs a real value in production +5. Trigger the first deploy + +Server functions and API routes run on Netlify Functions. For lower-latency request handling, see Netlify Edge Functions: https://docs.netlify.com/edge-functions/overview. diff --git a/packages/create/src/frameworks/solid/hosts/netlify/info.json b/packages/create/src/frameworks/solid/hosts/netlify/info.json index f0d67092..8da171fd 100644 --- a/packages/create/src/frameworks/solid/hosts/netlify/info.json +++ b/packages/create/src/frameworks/solid/hosts/netlify/info.json @@ -1,6 +1,6 @@ { "name": "Netlify", - "description": "Netlify deployment setup", + "description": "Deploy to Netlify (Functions and Edge Functions, GitHub-driven).", "link": "https://docs.netlify.com", "phase": "add-on", "modes": ["file-router", "code-router"], diff --git a/packages/create/src/frameworks/solid/hosts/nitro/README.md b/packages/create/src/frameworks/solid/hosts/nitro/README.md new file mode 100644 index 00000000..027037df --- /dev/null +++ b/packages/create/src/frameworks/solid/hosts/nitro/README.md @@ -0,0 +1,12 @@ +## Deploy with Nitro + +This project uses Nitro as a generic server adapter, so it can run on any Node-compatible host. + +```bash +npm run build +node dist/server/index.mjs +``` + +The build output is a self-contained Node server. To deploy, push the `dist/` directory to your host (Render, Fly.io, your own VPS, etc.) and run the server command above. + +For host-specific presets (Vercel, Netlify, Cloudflare, AWS Lambda, etc.) and tuning, see https://v3.nitro.build/deploy. diff --git a/packages/create/src/frameworks/solid/hosts/nitro/info.json b/packages/create/src/frameworks/solid/hosts/nitro/info.json index 9f89d465..1a564ec0 100644 --- a/packages/create/src/frameworks/solid/hosts/nitro/info.json +++ b/packages/create/src/frameworks/solid/hosts/nitro/info.json @@ -1,6 +1,6 @@ { "name": "Nitro (agnostic)", - "description": "Nitro deployment setup", + "description": "Generic Nitro adapter (deploy to any Node-compatible host).", "link": "https://v3.nitro.build/", "phase": "add-on", "modes": ["file-router", "code-router"], diff --git a/packages/create/src/frameworks/solid/hosts/railway/README.md b/packages/create/src/frameworks/solid/hosts/railway/README.md new file mode 100644 index 00000000..d60c1f46 --- /dev/null +++ b/packages/create/src/frameworks/solid/hosts/railway/README.md @@ -0,0 +1,10 @@ +## Deploy to Railway + +This project ships with `nixpacks.toml` so Railway detects the build automatically: + +1. Push this repo to GitHub +2. Visit https://railway.com/new and create a project from your repo +3. In the **Variables** tab, add the entries from `.env.example` with their production values +4. Railway runs `vite build` and serves from `dist/client` + +Need a database? Click **+ New** in your project to provision Postgres, MySQL, or Redis directly into the same environment — the connection string is auto-injected as `DATABASE_URL`. diff --git a/packages/create/src/frameworks/solid/hosts/railway/info.json b/packages/create/src/frameworks/solid/hosts/railway/info.json index cfd55309..df437f53 100644 --- a/packages/create/src/frameworks/solid/hosts/railway/info.json +++ b/packages/create/src/frameworks/solid/hosts/railway/info.json @@ -1,6 +1,6 @@ { "name": "Railway", - "description": "Railway deployment setup", + "description": "Deploy to Railway (full-stack PaaS for Node + databases).", "link": "https://railway.com/", "phase": "add-on", "modes": ["file-router", "code-router"], diff --git a/packages/create/src/index.ts b/packages/create/src/index.ts index 3f681c7d..ff2c43c5 100644 --- a/packages/create/src/index.ts +++ b/packages/create/src/index.ts @@ -51,6 +51,7 @@ export { export { cleanUpFiles, cleanUpFileArray, + isDemoFilePath, readFileHelper, getBinaryFile, recursivelyGatherFiles, diff --git a/packages/create/src/integrations/intent.ts b/packages/create/src/integrations/intent.ts index 208078e3..0dd6212f 100644 --- a/packages/create/src/integrations/intent.ts +++ b/packages/create/src/integrations/intent.ts @@ -27,7 +27,7 @@ export async function setupIntent( resolve(targetDir), options.packageManager, '@tanstack/intent', - ['install'], + ['install', '--map'], ) environment.finishStep('setup-intent', 'TanStack Intent configured') s.stop('TanStack Intent configured') diff --git a/packages/create/tests/create-app.test.ts b/packages/create/tests/create-app.test.ts index 6d77f1c5..e0182183 100644 --- a/packages/create/tests/create-app.test.ts +++ b/packages/create/tests/create-app.test.ts @@ -194,4 +194,99 @@ describe('createApp', () => { expect(output.files['/src/components/demo-AIAssistant.tsx']).toBeDefined() }) + + it('writes .env.example from add-on envVars metadata', async () => { + const { environment, output } = createMemoryEnvironment() + + await createApp(environment, { + ...simpleOptions, + chosenAddOns: [ + { + id: 'fake-auth', + name: 'Fake Auth', + type: 'add-on', + phase: 'add-on', + packageAdditions: { dependencies: {}, devDependencies: {} }, + routes: [], + integrations: [], + envVars: [ + { + name: 'AUTH_SECRET', + description: 'Random secret used to sign sessions', + required: true, + }, + { + name: 'AUTH_URL', + description: 'Base URL for auth callbacks', + required: false, + }, + ], + getFiles: () => [], + getFileContents: () => '', + getDeletedFiles: () => [], + } as unknown as AddOn, + ], + } as Options) + + const envExample = output.files['/.env.example'] + expect(envExample).toBeDefined() + expect(envExample).toContain('# Fake Auth') + expect(envExample).toContain( + '# Random secret used to sign sessions (required)', + ) + expect(envExample).toContain('AUTH_SECRET=') + expect(envExample).toContain('# Base URL for auth callbacks') + expect(envExample).toContain('AUTH_URL=') + }) + + it('does not write .env.example when no add-on declares envVars', async () => { + const { environment, output } = createMemoryEnvironment() + + await createApp(environment, { + ...simpleOptions, + chosenAddOns: [], + } as Options) + + expect(output.files['/.env.example']).toBeUndefined() + }) + + it('dedupes env vars across add-ons in .env.example', async () => { + const { environment, output } = createMemoryEnvironment() + + await createApp(environment, { + ...simpleOptions, + chosenAddOns: [ + { + id: 'addon-a', + name: 'Addon A', + type: 'add-on', + phase: 'add-on', + packageAdditions: { dependencies: {}, devDependencies: {} }, + routes: [], + integrations: [], + envVars: [{ name: 'SHARED_KEY', required: true }], + getFiles: () => [], + getFileContents: () => '', + getDeletedFiles: () => [], + } as unknown as AddOn, + { + id: 'addon-b', + name: 'Addon B', + type: 'add-on', + phase: 'add-on', + packageAdditions: { dependencies: {}, devDependencies: {} }, + routes: [], + integrations: [], + envVars: [{ name: 'SHARED_KEY', required: true }], + getFiles: () => [], + getFileContents: () => '', + getDeletedFiles: () => [], + } as unknown as AddOn, + ], + } as Options) + + const envExample = output.files['/.env.example'] || '' + const occurrences = envExample.match(/^SHARED_KEY=/gm) || [] + expect(occurrences.length).toBe(1) + }) })