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
JavaScript
#!/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();