create-solostack 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,592 @@
1
+ import path from 'path';
2
+ import { writeFile, renderTemplateToFile, getTemplatePath, ensureDir } from '../utils/files.js';
3
+ import { PACKAGE_VERSIONS } from '../constants.js';
4
+
5
+ /**
6
+ * Generates the base Next.js 15 project structure
7
+ * @param {string} projectPath - Path where the project should be generated
8
+ * @param {string} projectName - Name of the project
9
+ * @param {object} config - Project configuration
10
+ */
11
+ export async function generateBase(projectPath, projectName, config) {
12
+ // Create directory structure
13
+ await ensureDir(path.join(projectPath, 'src/app'));
14
+ await ensureDir(path.join(projectPath, 'src/components'));
15
+ await ensureDir(path.join(projectPath, 'src/lib'));
16
+ await ensureDir(path.join(projectPath, 'public'));
17
+
18
+ //Generate package.json
19
+ const packageJson = {
20
+ name: projectName,
21
+ version: '0.1.0',
22
+ private: true,
23
+ scripts: {
24
+ dev: 'next dev',
25
+ build: 'next build',
26
+ start: 'next start',
27
+ lint: 'next lint',
28
+ 'db:push': 'prisma db push',
29
+ 'db:seed': 'tsx prisma/seed.ts',
30
+ 'db:studio': 'prisma studio',
31
+ },
32
+ dependencies: {
33
+ next: PACKAGE_VERSIONS.next,
34
+ react: PACKAGE_VERSIONS.react,
35
+ 'react-dom': PACKAGE_VERSIONS.reactDom,
36
+ '@prisma/client': PACKAGE_VERSIONS['@prisma/client'],
37
+ 'next-auth': PACKAGE_VERSIONS['next-auth'],
38
+ bcryptjs: PACKAGE_VERSIONS.bcryptjs,
39
+ stripe: PACKAGE_VERSIONS.stripe,
40
+ resend: PACKAGE_VERSIONS.resend,
41
+ '@react-email/components': PACKAGE_VERSIONS['@react-email/components'],
42
+ zod: PACKAGE_VERSIONS.zod,
43
+ 'react-hook-form': PACKAGE_VERSIONS['react-hook-form'],
44
+ '@hookform/resolvers': PACKAGE_VERSIONS['@hookform/resolvers'],
45
+ 'class-variance-authority': PACKAGE_VERSIONS['class-variance-authority'],
46
+ clsx: PACKAGE_VERSIONS.clsx,
47
+ 'tailwind-merge': PACKAGE_VERSIONS['tailwind-merge'],
48
+ 'tailwindcss-animate': PACKAGE_VERSIONS['tailwindcss-animate'],
49
+ 'lucide-react': PACKAGE_VERSIONS['lucide-react'],
50
+ '@radix-ui/react-dropdown-menu': PACKAGE_VERSIONS['@radix-ui/react-dropdown-menu'],
51
+ '@radix-ui/react-slot': PACKAGE_VERSIONS['@radix-ui/react-slot'],
52
+ '@radix-ui/react-toast': PACKAGE_VERSIONS['@radix-ui/react-toast'],
53
+ '@radix-ui/react-dialog': PACKAGE_VERSIONS['@radix-ui/react-dialog'],
54
+ '@radix-ui/react-label': PACKAGE_VERSIONS['@radix-ui/react-label'],
55
+ '@auth/prisma-adapter': PACKAGE_VERSIONS['@auth/prisma-adapter'],
56
+ },
57
+ devDependencies: {
58
+ typescript: PACKAGE_VERSIONS.typescript,
59
+ '@types/node': PACKAGE_VERSIONS['@types/node'],
60
+ '@types/react': PACKAGE_VERSIONS['@types/react'],
61
+ '@types/react-dom': PACKAGE_VERSIONS['@types/react-dom'],
62
+ '@types/bcryptjs': PACKAGE_VERSIONS['@types/bcryptjs'],
63
+ tailwindcss: PACKAGE_VERSIONS.tailwindcss,
64
+ postcss: PACKAGE_VERSIONS.postcss,
65
+ autoprefixer: PACKAGE_VERSIONS.autoprefixer,
66
+ prisma: PACKAGE_VERSIONS.prisma,
67
+ tsx: '^4.19.2',
68
+ eslint: '^9',
69
+ 'eslint-config-next': '^15.1.6',
70
+ },
71
+ };
72
+
73
+ await writeFile(
74
+ path.join(projectPath, 'package.json'),
75
+ JSON.stringify(packageJson, null, 2)
76
+ );
77
+
78
+ // Generate tsconfig.json
79
+ const tsConfig = {
80
+ compilerOptions: {
81
+ target: 'ES2017',
82
+ lib: ['dom', 'dom.iterable', 'esnext'],
83
+ allowJs: true,
84
+ skipLibCheck: true,
85
+ strict: true,
86
+ noEmit: true,
87
+ esModuleInterop: true,
88
+ module: 'esnext',
89
+ moduleResolution: 'bundler',
90
+ resolveJsonModule: true,
91
+ isolatedModules: true,
92
+ jsx: 'preserve',
93
+ incremental: true,
94
+ plugins: [
95
+ {
96
+ name: 'next',
97
+ },
98
+ ],
99
+ paths: {
100
+ '@/*': ['./src/*'],
101
+ },
102
+ },
103
+ include: ['next-env.d.ts', '**/*.ts', '**/*.tsx', '.next/types/**/*.ts'],
104
+ exclude: ['node_modules'],
105
+ };
106
+
107
+ await writeFile(
108
+ path.join(projectPath, 'tsconfig.json'),
109
+ JSON.stringify(tsConfig, null, 2)
110
+ );
111
+
112
+ // Generate next.config.js
113
+ const nextConfig = `/** @type {import('next').NextConfig} */
114
+ const nextConfig = {
115
+ experimental: {
116
+ serverActions: {
117
+ bodySizeLimit: '2mb',
118
+ },
119
+ },
120
+ };
121
+
122
+ export default nextConfig;
123
+ `;
124
+
125
+ await writeFile(path.join(projectPath, 'next.config.js'), nextConfig);
126
+
127
+ // Generate tailwind.config.ts
128
+ const tailwindConfig = `import type { Config } from "tailwindcss";
129
+
130
+ const config: Config = {
131
+ darkMode: ["class"],
132
+ content: [
133
+ "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
134
+ "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
135
+ "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
136
+ ],
137
+ theme: {
138
+ extend: {
139
+ colors: {
140
+ background: "hsl(var(--background))",
141
+ foreground: "hsl(var(--foreground))",
142
+ card: {
143
+ DEFAULT: "hsl(var(--card))",
144
+ foreground: "hsl(var(--card-foreground))",
145
+ },
146
+ popover: {
147
+ DEFAULT: "hsl(var(--popover))",
148
+ foreground: "hsl(var(--popover-foreground))",
149
+ },
150
+ primary: {
151
+ DEFAULT: "hsl(var(--primary))",
152
+ foreground: "hsl(var(--primary-foreground))",
153
+ },
154
+ secondary: {
155
+ DEFAULT: "hsl(var(--secondary))",
156
+ foreground: "hsl(var(--secondary-foreground))",
157
+ },
158
+ muted: {
159
+ DEFAULT: "hsl(var(--muted))",
160
+ foreground: "hsl(var(--muted-foreground))",
161
+ },
162
+ accent: {
163
+ DEFAULT: "hsl(var(--accent))",
164
+ foreground: "hsl(var(--accent-foreground))",
165
+ },
166
+ destructive: {
167
+ DEFAULT: "hsl(var(--destructive))",
168
+ foreground: "hsl(var(--destructive-foreground))",
169
+ },
170
+ border: "hsl(var(--border))",
171
+ input: "hsl(var(--input))",
172
+ ring: "hsl(var(--ring))",
173
+ chart: {
174
+ "1": "hsl(var(--chart-1))",
175
+ "2": "hsl(var(--chart-2))",
176
+ "3": "hsl(var(--chart-3))",
177
+ "4": "hsl(var(--chart-4))",
178
+ "5": "hsl(var(--chart-5))",
179
+ },
180
+ },
181
+ borderRadius: {
182
+ lg: "var(--radius)",
183
+ md: "calc(var(--radius) - 2px)",
184
+ sm: "calc(var(--radius) - 4px)",
185
+ },
186
+ },
187
+ },
188
+ plugins: [require("tailwindcss-animate")],
189
+ };
190
+
191
+ export default config;
192
+ `;
193
+
194
+ await writeFile(path.join(projectPath, 'tailwind.config.ts'), tailwindConfig);
195
+
196
+ // Generate postcss.config.mjs
197
+ const postcssConfig = `/** @type {import('postcss-load-config').Config} */
198
+ const config = {
199
+ plugins: {
200
+ tailwindcss: {},
201
+ autoprefixer: {},
202
+ },
203
+ };
204
+
205
+ export default config;
206
+ `;
207
+
208
+ await writeFile(path.join(projectPath, 'postcss.config.mjs'), postcssConfig);
209
+
210
+ // Generate app/globals.css
211
+ const globalsCss = `@tailwind base;
212
+ @tailwind components;
213
+ @tailwind utilities;
214
+
215
+ @layer base {
216
+ :root {
217
+ --background: 0 0% 100%;
218
+ --foreground: 222.2 84% 4.9%;
219
+ --card: 0 0% 100%;
220
+ --card-foreground: 222.2 84% 4.9%;
221
+ --popover: 0 0% 100%;
222
+ --popover-foreground: 222.2 84% 4.9%;
223
+ --primary: 222.2 47.4% 11.2%;
224
+ --primary-foreground: 210 40% 98%;
225
+ --secondary: 210 40% 96.1%;
226
+ --secondary-foreground: 222.2 47.4% 11.2%;
227
+ --muted: 210 40% 96.1%;
228
+ --muted-foreground: 215.4 16.3% 46.9%;
229
+ --accent: 210 40% 96.1%;
230
+ --accent-foreground: 222.2 47.4% 11.2%;
231
+ --destructive: 0 84.2% 60.2%;
232
+ --destructive-foreground: 210 40% 98%;
233
+ --border: 214.3 31.8% 91.4%;
234
+ --input: 214.3 31.8% 91.4%;
235
+ --ring: 222.2 84% 4.9%;
236
+ --radius: 0.5rem;
237
+ --chart-1: 12 76% 61%;
238
+ --chart-2: 173 58% 39%;
239
+ --chart-3: 197 37% 24%;
240
+ --chart-4: 43 74% 66%;
241
+ --chart-5: 27 87% 67%;
242
+ }
243
+
244
+ .dark {
245
+ --background: 222.2 84% 4.9%;
246
+ --foreground: 210 40% 98%;
247
+ --card: 222.2 84% 4.9%;
248
+ --card-foreground: 210 40% 98%;
249
+ --popover: 222.2 84% 4.9%;
250
+ --popover-foreground: 210 40% 98%;
251
+ --primary: 210 40% 98%;
252
+ --primary-foreground: 222.2 47.4% 11.2%;
253
+ --secondary: 217.2 32.6% 17.5%;
254
+ --secondary-foreground: 210 40% 98%;
255
+ --muted: 217.2 32.6% 17.5%;
256
+ --muted-foreground: 215 20.2% 65.1%;
257
+ --accent: 217.2 32.6% 17.5%;
258
+ --accent-foreground: 210 40% 98%;
259
+ --destructive: 0 62.8% 30.6%;
260
+ --destructive-foreground: 210 40% 98%;
261
+ --border: 217.2 32.6% 17.5%;
262
+ --input: 217.2 32.6% 17.5%;
263
+ --ring: 212.7 26.8% 83.9%;
264
+ --chart-1: 220 70% 50%;
265
+ --chart-2: 160 60% 45%;
266
+ --chart-3: 30 80% 55%;
267
+ --chart-4: 280 65% 60%;
268
+ --chart-5: 340 75% 55%;
269
+ }
270
+ }
271
+
272
+ @layer base {
273
+ * {
274
+ @apply border-border;
275
+ }
276
+ body {
277
+ @apply bg-background text-foreground;
278
+ }
279
+ }
280
+ `;
281
+
282
+ await writeFile(path.join(projectPath, 'src/app/globals.css'), globalsCss);
283
+
284
+ // Generate app/layout.tsx
285
+ const layoutTsx = `import type { Metadata } from "next";
286
+ import { Inter } from "next/font/google";
287
+ import "./globals.css";
288
+
289
+ const inter = Inter({ subsets: ["latin"] });
290
+
291
+ export const metadata: Metadata = {
292
+ title: "${projectName}",
293
+ description: "Built with SoloStack - The complete SaaS boilerplate",
294
+ };
295
+
296
+ export default function RootLayout({
297
+ children,
298
+ }: Readonly<{
299
+ children: React.ReactNode;
300
+ }>) {
301
+ return (
302
+ <html lang="en">
303
+ <body className={inter.className}>{children}</body>
304
+ </html>
305
+ );
306
+ }
307
+ `;
308
+
309
+ await writeFile(path.join(projectPath, 'src/app/layout.tsx'), layoutTsx);
310
+
311
+ // Generate app/page.tsx
312
+ const pageTsx = `export default function Home() {
313
+ return (
314
+ <main className="flex min-h-screen flex-col items-center justify-center p-24">
315
+ <div className="z-10 max-w-5xl w-full items-center justify-center font-mono text-sm">
316
+ <h1 className="text-4xl font-bold text-center mb-4">
317
+ Welcome to ${projectName}
318
+ </h1>
319
+ <p className="text-center text-muted-foreground">
320
+ Built with SoloStack - Your Next.js SaaS boilerplate
321
+ </p>
322
+ <div className="mt-8 grid grid-cols-1 md:grid-cols-2 gap-4">
323
+ <div className="p-6 border rounded-lg">
324
+ <h2 className="text-xl font-semibold mb-2">✅ Authentication</h2>
325
+ <p className="text-sm text-muted-foreground">
326
+ NextAuth.js configured with email and OAuth providers
327
+ </p>
328
+ </div>
329
+ <div className="p-6 border rounded-lg">
330
+ <h2 className="text-xl font-semibold mb-2">✅ Database</h2>
331
+ <p className="text-sm text-muted-foreground">
332
+ Prisma + PostgreSQL ready to use
333
+ </p>
334
+ </div>
335
+ <div className="p-6 border rounded-lg">
336
+ <h2 className="text-xl font-semibold mb-2">✅ Payments</h2>
337
+ <p className="text-sm text-muted-foreground">
338
+ Stripe integration for subscriptions
339
+ </p>
340
+ </div>
341
+ <div className="p-6 border rounded-lg">
342
+ <h2 className="text-xl font-semibold mb-2">✅ Emails</h2>
343
+ <p className="text-sm text-muted-foreground">
344
+ Resend configured for transactional emails
345
+ </p>
346
+ </div>
347
+ </div>
348
+ <div className="mt-8 text-center">
349
+ <p className="text-sm text-muted-foreground">
350
+ Get started by editing <code className="font-mono bg-muted px-2 py-1 rounded">src/app/page.tsx</code>
351
+ </p>
352
+ </div>
353
+ </div>
354
+ </main>
355
+ );
356
+ }
357
+ `;
358
+
359
+ await writeFile(path.join(projectPath, 'src/app/page.tsx'), pageTsx);
360
+
361
+ // Generate .env.example
362
+ const envExample = `# Database
363
+ DATABASE_URL="postgresql://user:password@localhost:5432/dbname"
364
+ # For cloud databases, add ?sslmode=require to the connection string
365
+
366
+ # NextAuth
367
+ NEXTAUTH_SECRET="" # Generate with: openssl rand -base64 32
368
+ NEXTAUTH_URL="http://localhost:3000"
369
+ # In production, set to your domain: https://yourdomain.com
370
+
371
+ # Stripe
372
+ STRIPE_SECRET_KEY="sk_test_..."
373
+ STRIPE_WEBHOOK_SECRET="whsec_..."
374
+ NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY="pk_test_..."
375
+ # Create price IDs in Stripe Dashboard -> Products
376
+ STRIPE_PRO_PRICE_ID="price_..." # Monthly Pro plan price ID
377
+ STRIPE_ENTERPRISE_PRICE_ID="price_..." # Monthly Enterprise plan price ID
378
+
379
+ # Resend
380
+ RESEND_API_KEY="re_..."
381
+ FROM_EMAIL="onboarding@resend.dev" # Use your verified domain
382
+
383
+ # OAuth Providers (Optional)
384
+ # Google: https://console.cloud.google.com/apis/credentials
385
+ GOOGLE_CLIENT_ID=""
386
+ GOOGLE_CLIENT_SECRET=""
387
+ # GitHub: https://github.com/settings/developers
388
+ GITHUB_CLIENT_ID=""
389
+ GITHUB_CLIENT_SECRET=""
390
+ `;
391
+
392
+ await writeFile(path.join(projectPath, '.env.example'), envExample);
393
+
394
+ // Generate README.md
395
+ const readme = `# ${projectName}
396
+
397
+ Built with [SoloStack](https://github.com/yourusername/create-solostack) - The complete SaaS boilerplate for indie hackers.
398
+
399
+ ## Features
400
+
401
+ - ✅ **Next.js 15** with App Router
402
+ - ✅ **TypeScript** for type safety
403
+ - ✅ **Tailwind CSS** for styling
404
+ - ✅ **Prisma** + PostgreSQL for database
405
+ - ✅ **NextAuth.js** for authentication
406
+ - ✅ **Stripe** for payments
407
+ - ✅ **Resend** for emails
408
+ - ✅ **shadcn/ui** components
409
+
410
+ ## Getting Started
411
+
412
+ ### 1. Install dependencies
413
+
414
+ \`\`\`bash
415
+ npm install
416
+ \`\`\`
417
+
418
+ ### 2. Set up environment variables
419
+
420
+ \`\`\`bash
421
+ cp .env.example .env
422
+ \`\`\`
423
+
424
+ Edit \`.env\` and add your own values.
425
+
426
+ ### 3. Set up the database
427
+
428
+ \`\`\`bash
429
+ npm run db:push
430
+ npm run db:seed
431
+ \`\`\`
432
+
433
+ ### 4. Run the development server
434
+
435
+ \`\`\`bash
436
+ npm run dev
437
+ \`\`\`
438
+
439
+ Open [http://localhost:3000](http://localhost:3000) with your browser.
440
+
441
+ ## Project Structure
442
+
443
+ \`\`\`
444
+ ${projectName}/
445
+ ├── prisma/ # Database schema and migrations
446
+ ├── public/ # Static assets
447
+ ├── src/
448
+ │ ├── app/ # Next.js app directory
449
+ │ │ ├── api/ # API routes
450
+ │ │ ├── (auth)/ # Auth pages
451
+ │ │ └── dashboard/ # Protected pages
452
+ │ ├── components/ # React components
453
+ │ └── lib/ # Utilities and configurations
454
+ └── package.json
455
+ \`\`\`
456
+
457
+ ## Available Scripts
458
+
459
+ - \`npm run dev\` - Start development server
460
+ - \`npm run build\` - Build for production
461
+ - \`npm run start\` - Start production server
462
+ - \`npm run lint\` - Run ESLint
463
+ - \`npm run db:push\` - Push schema changes to database
464
+ - \`npm run db:seed\` - Seed database with sample data
465
+ - \`npm run db:studio\` - Open Prisma Studio
466
+
467
+ ## Learn More
468
+
469
+ - [Next.js Documentation](https://nextjs.org/docs)
470
+ - [NextAuth.js Documentation](https://next-auth.js.org)
471
+ - [Prisma Documentation](https://www.prisma.io/docs)
472
+ - [Stripe Documentation](https://stripe.com/docs)
473
+ - [Resend Documentation](https://resend.com/docs)
474
+
475
+ ## License
476
+
477
+ MIT
478
+ `;
479
+
480
+ await writeFile(path.join(projectPath, 'README.md'), readme);
481
+
482
+ // Create lib/utils.ts for cn helper
483
+ const utilsTs = `import { clsx, type ClassValue } from "clsx";
484
+ import { twMerge } from "tailwind-merge";
485
+
486
+ export function cn(...inputs: ClassValue[]) {
487
+ return twMerge(clsx(inputs));
488
+ }
489
+ `;
490
+
491
+ await writeFile(path.join(projectPath, 'src/lib/utils.ts'), utilsTs);
492
+
493
+ // Create next-auth.d.ts for type definitions
494
+ const nextAuthTypes = `import { DefaultSession } from "next-auth";
495
+
496
+ declare module "next-auth" {
497
+ interface Session {
498
+ user: {
499
+ id: string;
500
+ role: string;
501
+ } & DefaultSession["user"];
502
+ }
503
+ }
504
+
505
+ declare module "next-auth/jwt" {
506
+ interface JWT {
507
+ id: string;
508
+ role: string;
509
+ }
510
+ }
511
+ `;
512
+
513
+ await writeFile(path.join(projectPath, 'next-auth.d.ts'), nextAuthTypes);
514
+
515
+ // Create global error boundary
516
+ const errorPage = `'use client';
517
+
518
+ import { useEffect } from 'react';
519
+
520
+ export default function Error({
521
+ error,
522
+ reset,
523
+ }: {
524
+ error: Error & { digest?: string };
525
+ reset: () => void;
526
+ }) {
527
+ useEffect(() => {
528
+ console.error('Application error:', error);
529
+ }, [error]);
530
+
531
+ return (
532
+ <div className="flex min-h-screen flex-col items-center justify-center bg-gray-50 px-4">
533
+ <div className="w-full max-w-md text-center">
534
+ <h1 className="text-4xl font-bold text-gray-900 mb-4">Something went wrong!</h1>
535
+ <p className="text-gray-600 mb-8">
536
+ We're sorry, but something unexpected happened. Please try again.
537
+ </p>
538
+ <button
539
+ onClick={() => reset()}
540
+ className="rounded-md bg-indigo-600 px-4 py-2 text-sm font-semibold text-white hover:bg-indigo-500"
541
+ >
542
+ Try again
543
+ </button>
544
+ </div>
545
+ </div>
546
+ );
547
+ }
548
+ `;
549
+
550
+ await writeFile(path.join(projectPath, 'src/app/error.tsx'), errorPage);
551
+
552
+ // Create not-found page
553
+ const notFoundPage = `import Link from 'next/link';
554
+
555
+ export default function NotFound() {
556
+ return (
557
+ <div className="flex min-h-screen flex-col items-center justify-center bg-gray-50 px-4">
558
+ <div className="w-full max-w-md text-center">
559
+ <h1 className="text-6xl font-bold text-gray-900 mb-4">404</h1>
560
+ <h2 className="text-2xl font-semibold text-gray-700 mb-4">Page Not Found</h2>
561
+ <p className="text-gray-600 mb-8">
562
+ The page you're looking for doesn't exist or has been moved.
563
+ </p>
564
+ <Link
565
+ href="/"
566
+ className="inline-block rounded-md bg-indigo-600 px-4 py-2 text-sm font-semibold text-white hover:bg-indigo-500"
567
+ >
568
+ Go Home
569
+ </Link>
570
+ </div>
571
+ </div>
572
+ );
573
+ }
574
+ `;
575
+
576
+ await writeFile(path.join(projectPath, 'src/app/not-found.tsx'), notFoundPage);
577
+
578
+ // Create root loading state
579
+ const loadingPage = `export default function Loading() {
580
+ return (
581
+ <div className="flex min-h-screen items-center justify-center bg-gray-50">
582
+ <div className="text-center">
583
+ <div className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-indigo-600 border-r-transparent"></div>
584
+ <p className="mt-4 text-sm text-gray-600">Loading...</p>
585
+ </div>
586
+ </div>
587
+ );
588
+ }
589
+ `;
590
+
591
+ await writeFile(path.join(projectPath, 'src/app/loading.tsx'), loadingPage);
592
+ }