UNPKG

god-nextapp-project

Version:

CLI to initialize a Next.js project with Prisma, Tailwind v4, Clerk, TypeScript, shadcn (neutral theme), and custom folders.

644 lines (566 loc) 19.1 kB
#!/usr/bin/env node import { Command } from "commander"; import { execa } from "execa"; import fs from "fs-extra"; import ora from "ora"; import path from "path"; import chalk from "chalk"; import { fileURLToPath } from "url"; const program = new Command(); const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const run = async (cmd, args, opts = {}) => { await execa(cmd, args, { stdio: "inherit", ...opts }); }; const write = async (file, content) => { await fs.ensureDir(path.dirname(file)); await fs.writeFile(file, content); }; program .name("create-super-next") .description( "Scaffold Next.js + shadcn + Clerk + Prisma + Jest + custom structure" ) .argument("<project-name>", "name of your Next.js project") .action(async (projectName) => { const spinner = ora("Scaffolding...").start(); const projectPath = path.join(process.cwd(), projectName); try { // 1) Create Next app (TS, ESLint, Tailwind, src, app router) spinner.text = "Creating Next.js app..."; await run("npx", [ "create-next-app@latest", projectName === "./" ? "." : projectName, "--ts", "--eslint", "--tailwind", "--src-dir", "--app", "--import-alias", "@/*", "--yes", ]); // 2) Install deps spinner.text = "Installing dependencies..."; const deps = [ "@clerk/nextjs", "next-themes", "framer-motion", "@prisma/client", "prisma", ]; const devDeps = [ "@testing-library/jest-dom", "@testing-library/react", "@testing-library/user-event", "@types/jest", "jest-environment-jsdom", "ts-jest", "jest", "eslint-plugin-testing-library", "eslint-plugin-jest-dom", ]; await run("npm", ["install", ...deps], { cwd: projectPath }); await run("npm", ["install", "-D", ...devDeps], { cwd: projectPath }); await run("npm", ["install", ...deps, "-D", ...devDeps], { cwd: projectPath, }); // 3) shadcn/ui init (neutral theme) spinner.text = "Initializing shadcn/ui..."; await run("npx", ["shadcn@latest", "init"], { cwd: projectPath }); // Add a couple of base components to ensure setup works await run("npx", ["shadcn@latest", "add", "button", "card"], { cwd: projectPath, }); // 4) Prisma init spinner.text = "Initializing Prisma..."; await run("npx", ["prisma", "init"], { cwd: projectPath }); // 5) Move globals.css and fix imports spinner.text = "Rewiring globals.css..."; const appDir = path.join(projectPath, "src", "app"); const oldGlobals = path.join(appDir, "globals.css"); const newGlobals = path.join(projectPath, "src", "css", "globals.css"); if (await fs.pathExists(oldGlobals)) { await fs.ensureDir(path.dirname(newGlobals)); await fs.move(oldGlobals, newGlobals, { overwrite: true }); } // Update layout.tsx import const layoutPath = path.join(appDir, "layout.tsx"); let layoutSrc = await fs.readFile(layoutPath, "utf-8"); layoutSrc = layoutSrc.replace( /import '.\/globals\.css';?/, "import '@/css/globals.css';" ); await fs.writeFile(layoutPath, layoutSrc, "utf-8"); // 6) Create folders spinner.text = "Creating custom folders and files..."; const folders = [ "src/data", "src/types", "src/__tests__", "src/hooks", "src/page", "src/section", "src/helper", "src/context", "src/config", ".github/workflows", ".vscode", "public/assets", "public/images", "public/fonts", ]; for (const f of folders) await fs.ensureDir(path.join(projectPath, f)); // 7) Next config (images from any domain) await write( path.join(projectPath, "next.config.mjs"), `/** @type {import('next').NextConfig} */ const nextConfig = { images: { remotePatterns: [{ protocol: 'https', hostname: '**' }] } }; export default nextConfig; ` ); // 8) env example (dummy) await write( path.join(projectPath, ".env.local"), `# --- Clerk --- NEXT_PUBLIC_CLERK_KEY= NEXT_PUBLIC_CLERK_SECRET= # --- Database --- DATABASE_URL=postgresql://user:password@localhost:5432/dbname # --- App URLs --- NEXT_PUBLIC_BASE_URL=http://localhost:3000 NEXT_PUBLIC_PROD_URL=https://yourdomain.com # --- Environment flag --- NEXT_PUBLIC_ENV=development ` ); await write( path.join(projectPath, ".env.example"), `# --- Clerk --- NEXT_PUBLIC_CLERK_KEY= NEXT_PUBLIC_CLERK_SECRET= # --- Database --- DATABASE_URL=postgresql://user:password@localhost:5432/dbname # --- App URLs --- NEXT_PUBLIC_BASE_URL=http://localhost:3000 NEXT_PUBLIC_PROD_URL=https://yourdomain.com # --- Environment flag --- NEXT_PUBLIC_ENV=development ` ); await write( path.join(projectPath, ".env"), `# --- Clerk --- NEXT_PUBLIC_CLERK_KEY= NEXT_PUBLIC_CLERK_SECRET= # --- Database --- DATABASE_URL=postgresql://user:password@localhost:5432/dbname # --- App URLs --- NEXT_PUBLIC_BASE_URL=http://localhost:3000 NEXT_PUBLIC_PROD_URL=https://yourdomain.com # --- Environment flag --- NEXT_PUBLIC_ENV=development ` ); // 9) Prisma schema minimal await write( path.join(projectPath, "prisma", "schema.prisma"), `generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } model Example { id String @id @default(cuid()) createdAt DateTime @default(now()) name String } ` ); // 10) Prisma client helper await write( path.join(projectPath, "src", "lib", "prisma.ts"), `import { PrismaClient } from "@prisma/client"; const globalForPrisma = globalThis as unknown as { prisma: PrismaClient }; export const prisma = globalForPrisma.prisma ?? new PrismaClient(); if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma; ` ); // 11) Clerk middleware and Provider await write( path.join(projectPath, "src", "middleware.ts"), `import { clerkMiddleware } from '@clerk/nextjs/server' export default clerkMiddleware() export const config = { matcher: [ '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)', '/(api|trpc)(.*)', ], } ` ); await write( path.join(projectPath, "src", "app", "api", "route.ts"), `export async function GET() { return new Response("OK"); } ` ); // 12) Update root layout to use ClerkProvider + custom providers + Navbar/Footer const rootLayout = `import type { Metadata } from "next"; import { ClerkProvider } from "@clerk/nextjs"; import "@/css/globals.css"; import { ThemeProvider } from "@/context/theme-provider"; import { CustomProvider } from "@/context/custom-provider"; import Navbar from "@/components/navbar"; import Footer from "@/components/footer"; export const metadata: Metadata = { title: "GodNextApp", description: "Everything in one place at one click", }; export default function RootLayout({ children }: ChildProps) { return ( <ClerkProvider publishableKey={process.env.NEXT_PUBLIC_CLERK_KEY}> <html lang="en" suppressHydrationWarning> <body> <ThemeProvider attribute="class" defaultTheme="system" enableSystem> <CustomProvider> <Navbar /> {children} <Footer /> </CustomProvider> </ThemeProvider> </body> </html> </ClerkProvider> ); } `; await write(path.join(appDir, "layout.tsx"), rootLayout); // 13) Types: ChildProps await write( path.join(projectPath, "src", "types", "main.type.ts"), `export type ChildProps = { children: React.ReactNode }; export type ServiceConfig = { apiBase: string }; ` ); // 14) context providers await write( path.join(projectPath, "src", "context", "theme-provider.tsx"), `"use client"; import * as React from "react"; import { ThemeProvider as NextThemesProvider } from "next-themes"; import type { ChildProps } from "@/types/main.type"; export function ThemeProvider({ children, ...props }: ChildProps & React.ComponentProps<typeof NextThemesProvider>) { return <NextThemesProvider {...props}>{children}</NextThemesProvider>; } ` ); await write( path.join(projectPath, "src", "context", "custom-provider.tsx"), `"use client"; import * as React from "react"; import type { ChildProps } from "@/types/main.type"; const CustomContext = React.createContext<{ value: string }>({ value: "ok" }); export function CustomProvider({ children }: ChildProps) { return <CustomContext.Provider value={{ value: "ok" }}>{children}</CustomContext.Provider>; } export function useCustom() { return React.useContext(CustomContext); } ` ); // 15) components: Navbar/Footer (using shadcn button/card imported earlier) await fs.ensureDir(path.join(projectPath, "src", "components")); await write( path.join(projectPath, "src", "components", "navbar.tsx"), `"use client"; import Link from "next/link"; export default function Navbar() { return ( <header className="w-full border-b"> <div className="mx-auto max-w-6xl px-4 py-3 flex items-center justify-between"> <Link href="/" className="font-bold">Godnext</Link> </div> </header> ); } ` ); await write( path.join(projectPath, "src", "components", "footer.tsx"), `import Link from "next/link"; export default function Footer() { return ( <footer className="w-full flex items-center justify-center mt-10"> <div className="mx-auto max-w-6xl px-4 py-6 text-sm text-muted-foreground"> © 2025 <Link href={"https://meard.me"}>Godard</Link> </div> </footer> ); } ` ); // 16) page structure: Hero -> Home -> app/page await write( path.join(projectPath, "src", "section", "hero.tsx"), `"use client"; import { motion } from "framer-motion"; export default function Hero() { return ( <section className="mx-auto max-w-6xl px-4 py-20 text-center"> <motion.h1 initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.6, ease: "easeOut" }} className="text-4xl md:text-6xl font-extrabold tracking-tight bg-gradient-to-r from-teal-400 to-cyan-500 text-transparent bg-clip-text" > Welcome to create-god-nextapp-project but Arunava Dutta </motion.h1> <motion.p initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ delay: 0.3, duration: 0.6 }} className="mt-6 text-lg md:text-xl text-gray-400 max-w-3xl mx-auto" > Next.js + shadcn + Clerk + Prisma + Jest + Typescript + Tailwind + Proper custom folders — bootstrapped by one CLI. </motion.p> <motion.a href="https://meard.me" target="_blank" rel="noopener noreferrer" initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.5, duration: 0.6 }} className="mt-8 inline-block rounded-full bg-gradient-to-r from-teal-400 to-cyan-500 px-6 py-3 text-white font-semibold shadow-lg hover:scale-105 hover:shadow-xl transition-transform duration-300" > Visit My Website </motion.a> </section> ); } ` ); await write( path.join(projectPath, "src", "page", "home.tsx"), `import Hero from "@/section/hero"; export default function HomePage() { return <Hero />; } ` ); await write( path.join(projectPath, "src", "app", "page.tsx"), `import HomePage from "@/page/home"; export default function Page() { return <HomePage />; } ` ); // 17) data, hooks, helper, config await write( path.join(projectPath, "src", "data", "main.data.ts"), `export const site = { name: "GodNext" }; ` ); await write( path.join(projectPath, "src", "hooks", "use-hook.ts"), `"use client"; import * as React from "react"; export function useHook() { const [v, setV] = React.useState(0); return { v, setV }; } ` ); await write( path.join(projectPath, "src", "helper", "helper.tsx"), `export const greet = (name: string) => \`Hello, \${name}\`; ` ); await write( path.join(projectPath, "src", "config", "service.tsx"), `import type { ServiceConfig } from "@/types/main.type"; export const serviceConfig: ServiceConfig = { apiBase: process.env.NEXT_PUBLIC_BASE_URL ?? "http://localhost:3000" }; ` ); // 18) Clerk auth routes in (auth) folder const authDir = path.join(projectPath, "src", "app", "(auth)"); await fs.ensureDir(authDir); await write( path.join(authDir, "sign-in", "[[...sign-in]]", "page.tsx"), `"use client"; import { SignIn } from "@clerk/nextjs"; export default function Page() { return ( <div className="min-h-[60vh] flex items-center justify-center p-6"> <SignIn signUpUrl="/sign-up" /> </div> ); } ` ); await write( path.join(authDir, "sign-up", "[[...sign-up]]", "page.tsx"), `"use client"; import { SignUp } from "@clerk/nextjs"; export default function Page() { return ( <div className="min-h-[60vh] flex items-center justify-center p-6"> <SignUp signInUrl="/sign-in" /> </div> ); } ` ); // 19) Jest config + setup await write( path.join(projectPath, "jest.config.ts"), `import nextJest from "next/jest.js"; const createJestConfig = nextJest({ dir: "./" }); const config = { testEnvironment: "jest-environment-jsdom", setupFilesAfterEnv: ["<rootDir>/jest.setup.ts"], moduleNameMapper: { "^@/(.*)$": "<rootDir>/src/$1" } }; export default createJestConfig(config); ` ); await write( path.join(projectPath, "jest.setup.ts"), `import "@testing-library/jest-dom";` ); await write( path.join(projectPath, "src", "__tests__", "main.test.ts"), `import { greet } from "@/helper/helper"; describe("greet", () => { it("greets by name", () => { expect(greet("Arunava")).toBe("Hello, Arunava"); }); }); ` ); // 20) ESLint add testing-library/jest-dom (extends & plugins if needed) const eslintrcPath = path.join(projectPath, ".eslintrc.json"); if (await fs.pathExists(eslintrcPath)) { const eslintrc = JSON.parse(await fs.readFile(eslintrcPath, "utf-8")); eslintrc.plugins = Array.from( new Set([...(eslintrc.plugins ?? []), "testing-library", "jest-dom"]) ); eslintrc.extends = Array.from( new Set([ ...(eslintrc.extends ?? []), "plugin:testing-library/react", "plugin:jest-dom/recommended", ]) ); await fs.writeFile(eslintrcPath, JSON.stringify(eslintrc, null, 2)); } // 21) .github workflow and .vscode await write( path.join(projectPath, ".github", "workflows", "ci.yml"), `name: CI on: [push, pull_request] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 cache: npm - run: npm ci - run: npm run build --if-present - run: npm test -- --ci ` ); await write( path.join(projectPath, ".vscode", "settings.json"), `{ "typescript.tsdk": "node_modules/typescript/lib", "editor.formatOnSave": true } ` ); await write( path.join(projectPath, ".vscode", "extensions.json"), `{ "recommendations": [ "esbenp.prettier-vscode", "dbaeumer.vscode-eslint" ] } ` ); // 22) vercel.json await write( path.join(projectPath, "vercel.json"), `{ "version": 2, "buildCommand": "npm run build", "installCommand": "npm ci", "outputDirectory": ".next", "framework": "nextjs" } ` ); // 23) Tailwind content includes src and shadcn paths (usually already set, ensure css path ok) const twPath = path.join(projectPath, "tailwind.config.ts"); if (await fs.pathExists(twPath)) { let tw = await fs.readFile(twPath, "utf-8"); // Just ensure content has src/**/* (create-next-app already does) if (!tw.includes("./src/**/*")) { tw = tw.replace( /content:\s*\[([^\]]+)\]/m, `content: ["./src/**/*.{js,ts,jsx,tsx,mdx}"]` ); await fs.writeFile(twPath, tw, "utf-8"); } } // 24) Update app/globals import already done; ensure ChildProps global // Add global type import at top of layout/page files if needed const addTypeHeader = async (file) => { const src = await fs.readFile(file, "utf-8"); if (!src.includes("import type { ChildProps }")) { await fs.writeFile( file, `import type { ChildProps } from "@/types/main.type";\n` + src, "utf-8" ); } }; await addTypeHeader(path.join(appDir, "layout.tsx")); // 25) Add basic npm scripts for test const pkgPath = path.join(projectPath, "package.json"); const pkg = JSON.parse(await fs.readFile(pkgPath, "utf-8")); pkg.scripts = { ...pkg.scripts, test: "jest", "test:watch": "jest --watch", "db:push": "prisma db push", "db:studio": "prisma studio", }; await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2)); spinner.succeed(chalk.green("All done!")); console.log(chalk.cyan(`\nNext steps:`)); console.log(` cd ${projectName}`); console.log(` cp .env.local .env`); console.log(` npm run dev\n`); console.log(chalk.gray(`Auth routes: /sign-in and /sign-up`)); } catch (e) { spinner.fail("Scaffold failed"); console.error(e); process.exit(1); } }); program.parse();