548 lines
14 KiB
Python
548 lines
14 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Next.js Project Initialization Script
|
|
|
|
Initialize new Next.js project with best practices, TypeScript, and optimized configuration.
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
|
|
class NextJSInitializer:
|
|
"""Initialize Next.js project with best practices."""
|
|
|
|
def __init__(
|
|
self,
|
|
name: str,
|
|
directory: Optional[Path] = None,
|
|
typescript: bool = True,
|
|
app_router: bool = True,
|
|
src_dir: bool = False,
|
|
tailwind: bool = False,
|
|
eslint: bool = True,
|
|
import_alias: str = "@/*",
|
|
):
|
|
"""
|
|
Initialize NextJSInitializer.
|
|
|
|
Args:
|
|
name: Project name
|
|
directory: Target directory (default: current directory / name)
|
|
typescript: Enable TypeScript
|
|
app_router: Use App Router (recommended)
|
|
src_dir: Use src/ directory
|
|
tailwind: Include Tailwind CSS
|
|
eslint: Include ESLint
|
|
import_alias: Import alias pattern
|
|
"""
|
|
self.name = name
|
|
self.directory = directory or Path.cwd() / name
|
|
self.typescript = typescript
|
|
self.app_router = app_router
|
|
self.src_dir = src_dir
|
|
self.tailwind = tailwind
|
|
self.eslint = eslint
|
|
self.import_alias = import_alias
|
|
|
|
def validate_name(self) -> None:
|
|
"""Validate project name."""
|
|
if not self.name:
|
|
raise ValueError("Project name cannot be empty")
|
|
|
|
if not self.name.replace("-", "").replace("_", "").isalnum():
|
|
raise ValueError(
|
|
"Project name can only contain letters, numbers, hyphens, and underscores"
|
|
)
|
|
|
|
if self.name[0].isdigit():
|
|
raise ValueError("Project name cannot start with a number")
|
|
|
|
def check_directory(self) -> None:
|
|
"""Check if target directory exists."""
|
|
if self.directory.exists():
|
|
raise FileExistsError(f"Directory '{self.directory}' already exists")
|
|
|
|
def create_directory_structure(self) -> None:
|
|
"""Create project directory structure."""
|
|
print(f"Creating directory structure in {self.directory}...")
|
|
|
|
# Create base directories
|
|
self.directory.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Determine app/pages directory location
|
|
base_dir = self.directory / "src" if self.src_dir else self.directory
|
|
|
|
if self.app_router:
|
|
app_dir = base_dir / "app"
|
|
app_dir.mkdir(parents=True, exist_ok=True)
|
|
(app_dir / "favicon.ico").touch()
|
|
self._create_app_router_files(app_dir)
|
|
else:
|
|
pages_dir = base_dir / "pages"
|
|
pages_dir.mkdir(parents=True, exist_ok=True)
|
|
self._create_pages_router_files(pages_dir)
|
|
|
|
# Create additional directories
|
|
(self.directory / "public").mkdir(exist_ok=True)
|
|
(base_dir / "components").mkdir(parents=True, exist_ok=True)
|
|
(base_dir / "lib").mkdir(parents=True, exist_ok=True)
|
|
|
|
def _create_app_router_files(self, app_dir: Path) -> None:
|
|
"""Create App Router files."""
|
|
ext = "tsx" if self.typescript else "jsx"
|
|
|
|
# Create layout
|
|
layout_content = self._get_layout_content()
|
|
(app_dir / f"layout.{ext}").write_text(layout_content)
|
|
|
|
# Create page
|
|
page_content = self._get_page_content()
|
|
(app_dir / f"page.{ext}").write_text(page_content)
|
|
|
|
# Create global styles
|
|
if self.tailwind:
|
|
globals_content = self._get_tailwind_globals()
|
|
else:
|
|
globals_content = self._get_basic_globals()
|
|
(app_dir / "globals.css").write_text(globals_content)
|
|
|
|
def _create_pages_router_files(self, pages_dir: Path) -> None:
|
|
"""Create Pages Router files."""
|
|
ext = "tsx" if self.typescript else "jsx"
|
|
|
|
# Create _app
|
|
app_content = self._get_app_content()
|
|
(pages_dir / f"_app.{ext}").write_text(app_content)
|
|
|
|
# Create index
|
|
index_content = self._get_index_content()
|
|
(pages_dir / f"index.{ext}").write_text(index_content)
|
|
|
|
def create_config_files(self) -> None:
|
|
"""Create configuration files."""
|
|
print("Creating configuration files...")
|
|
|
|
# package.json
|
|
package_json = self._get_package_json()
|
|
(self.directory / "package.json").write_text(
|
|
json.dumps(package_json, indent=2)
|
|
)
|
|
|
|
# next.config.js
|
|
next_config = self._get_next_config()
|
|
(self.directory / "next.config.js").write_text(next_config)
|
|
|
|
# tsconfig.json
|
|
if self.typescript:
|
|
tsconfig = self._get_tsconfig()
|
|
(self.directory / "tsconfig.json").write_text(
|
|
json.dumps(tsconfig, indent=2)
|
|
)
|
|
|
|
# .eslintrc.json
|
|
if self.eslint:
|
|
eslint_config = self._get_eslint_config()
|
|
(self.directory / ".eslintrc.json").write_text(
|
|
json.dumps(eslint_config, indent=2)
|
|
)
|
|
|
|
# tailwind.config
|
|
if self.tailwind:
|
|
tailwind_config = self._get_tailwind_config()
|
|
ext = "ts" if self.typescript else "js"
|
|
(self.directory / f"tailwind.config.{ext}").write_text(tailwind_config)
|
|
|
|
postcss_config = self._get_postcss_config()
|
|
(self.directory / "postcss.config.js").write_text(postcss_config)
|
|
|
|
# .gitignore
|
|
gitignore = self._get_gitignore()
|
|
(self.directory / ".gitignore").write_text(gitignore)
|
|
|
|
# README.md
|
|
readme = self._get_readme()
|
|
(self.directory / "README.md").write_text(readme)
|
|
|
|
def _get_package_json(self) -> dict:
|
|
"""Generate package.json content."""
|
|
dependencies = {
|
|
"next": "latest",
|
|
"react": "latest",
|
|
"react-dom": "latest",
|
|
}
|
|
|
|
dev_dependencies = {}
|
|
|
|
if self.typescript:
|
|
dev_dependencies.update(
|
|
{
|
|
"typescript": "^5.0.0",
|
|
"@types/node": "^20.0.0",
|
|
"@types/react": "^18.0.0",
|
|
"@types/react-dom": "^18.0.0",
|
|
}
|
|
)
|
|
|
|
if self.eslint:
|
|
dev_dependencies["eslint"] = "^8.0.0"
|
|
dev_dependencies["eslint-config-next"] = "latest"
|
|
|
|
if self.tailwind:
|
|
dependencies["tailwindcss"] = "^3.3.0"
|
|
dependencies["autoprefixer"] = "^10.0.0"
|
|
dependencies["postcss"] = "^8.0.0"
|
|
|
|
return {
|
|
"name": self.name,
|
|
"version": "0.1.0",
|
|
"private": True,
|
|
"scripts": {
|
|
"dev": "next dev",
|
|
"build": "next build",
|
|
"start": "next start",
|
|
"lint": "next lint" if self.eslint else None,
|
|
},
|
|
"dependencies": dependencies,
|
|
"devDependencies": dev_dependencies,
|
|
}
|
|
|
|
def _get_layout_content(self) -> str:
|
|
"""Generate layout.tsx content."""
|
|
import_css = (
|
|
"import './globals.css'\n" if not self.tailwind else "import './globals.css'\n"
|
|
)
|
|
|
|
if self.typescript:
|
|
return f"""{import_css}
|
|
export const metadata = {{
|
|
title: '{self.name}',
|
|
description: 'Generated by Next.js',
|
|
}}
|
|
|
|
export default function RootLayout({{
|
|
children,
|
|
}}: {{
|
|
children: React.ReactNode
|
|
}}) {{
|
|
return (
|
|
<html lang="en">
|
|
<body>{{children}}</body>
|
|
</html>
|
|
)
|
|
}}
|
|
"""
|
|
return f"""{import_css}
|
|
export const metadata = {{
|
|
title: '{self.name}',
|
|
description: 'Generated by Next.js',
|
|
}}
|
|
|
|
export default function RootLayout({{ children }}) {{
|
|
return (
|
|
<html lang="en">
|
|
<body>{{children}}</body>
|
|
</html>
|
|
)
|
|
}}
|
|
"""
|
|
|
|
def _get_page_content(self) -> str:
|
|
"""Generate page.tsx content."""
|
|
return """export default function Home() {
|
|
return (
|
|
<main>
|
|
<h1>Welcome to Next.js!</h1>
|
|
<p>Get started by editing this page.</p>
|
|
</main>
|
|
)
|
|
}
|
|
"""
|
|
|
|
def _get_next_config(self) -> str:
|
|
"""Generate next.config.js content."""
|
|
return """/** @type {import('next').NextConfig} */
|
|
const nextConfig = {
|
|
reactStrictMode: true,
|
|
images: {
|
|
remotePatterns: [
|
|
// Add your image domains here
|
|
],
|
|
},
|
|
}
|
|
|
|
module.exports = nextConfig
|
|
"""
|
|
|
|
def _get_tsconfig(self) -> dict:
|
|
"""Generate tsconfig.json content."""
|
|
return {
|
|
"compilerOptions": {
|
|
"target": "ES2020",
|
|
"lib": ["dom", "dom.iterable", "esnext"],
|
|
"allowJs": True,
|
|
"skipLibCheck": True,
|
|
"strict": True,
|
|
"noEmit": True,
|
|
"esModuleInterop": True,
|
|
"module": "esnext",
|
|
"moduleResolution": "bundler",
|
|
"resolveJsonModule": True,
|
|
"isolatedModules": True,
|
|
"jsx": "preserve",
|
|
"incremental": True,
|
|
"plugins": [{"name": "next"}],
|
|
"paths": {self.import_alias: ["./*"]},
|
|
},
|
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
"exclude": ["node_modules"],
|
|
}
|
|
|
|
def _get_eslint_config(self) -> dict:
|
|
"""Generate .eslintrc.json content."""
|
|
return {"extends": "next/core-web-vitals"}
|
|
|
|
def _get_tailwind_config(self) -> str:
|
|
"""Generate tailwind.config content."""
|
|
if self.typescript:
|
|
return """import type { Config } from 'tailwindcss'
|
|
|
|
const config: Config = {
|
|
content: [
|
|
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
|
|
'./components/**/*.{js,ts,jsx,tsx,mdx}',
|
|
'./app/**/*.{js,ts,jsx,tsx,mdx}',
|
|
],
|
|
theme: {
|
|
extend: {},
|
|
},
|
|
plugins: [],
|
|
}
|
|
export default config
|
|
"""
|
|
return """/** @type {import('tailwindcss').Config} */
|
|
module.exports = {
|
|
content: [
|
|
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
|
|
'./components/**/*.{js,ts,jsx,tsx,mdx}',
|
|
'./app/**/*.{js,ts,jsx,tsx,mdx}',
|
|
],
|
|
theme: {
|
|
extend: {},
|
|
},
|
|
plugins: [],
|
|
}
|
|
"""
|
|
|
|
def _get_postcss_config(self) -> str:
|
|
"""Generate postcss.config.js content."""
|
|
return """module.exports = {
|
|
plugins: {
|
|
tailwindcss: {},
|
|
autoprefixer: {},
|
|
},
|
|
}
|
|
"""
|
|
|
|
def _get_tailwind_globals(self) -> str:
|
|
"""Generate globals.css with Tailwind."""
|
|
return """@tailwind base;
|
|
@tailwind components;
|
|
@tailwind utilities;
|
|
"""
|
|
|
|
def _get_basic_globals(self) -> str:
|
|
"""Generate basic globals.css."""
|
|
return """* {
|
|
box-sizing: border-box;
|
|
padding: 0;
|
|
margin: 0;
|
|
}
|
|
|
|
html,
|
|
body {
|
|
max-width: 100vw;
|
|
overflow-x: hidden;
|
|
}
|
|
|
|
a {
|
|
color: inherit;
|
|
text-decoration: none;
|
|
}
|
|
"""
|
|
|
|
def _get_gitignore(self) -> str:
|
|
"""Generate .gitignore content."""
|
|
return """# dependencies
|
|
/node_modules
|
|
/.pnp
|
|
.pnp.js
|
|
|
|
# testing
|
|
/coverage
|
|
|
|
# next.js
|
|
/.next/
|
|
/out/
|
|
|
|
# production
|
|
/build
|
|
|
|
# misc
|
|
.DS_Store
|
|
*.pem
|
|
|
|
# debug
|
|
npm-debug.log*
|
|
yarn-debug.log*
|
|
yarn-error.log*
|
|
|
|
# local env files
|
|
.env*.local
|
|
|
|
# vercel
|
|
.vercel
|
|
|
|
# typescript
|
|
*.tsbuildinfo
|
|
next-env.d.ts
|
|
"""
|
|
|
|
def _get_readme(self) -> str:
|
|
"""Generate README.md content."""
|
|
return f"""# {self.name}
|
|
|
|
This is a [Next.js](https://nextjs.org/) project bootstrapped with next.js initialization script.
|
|
|
|
## Getting Started
|
|
|
|
First, install dependencies:
|
|
|
|
```bash
|
|
npm install
|
|
# or
|
|
yarn install
|
|
# or
|
|
pnpm install
|
|
```
|
|
|
|
Then, run the development server:
|
|
|
|
```bash
|
|
npm run dev
|
|
# or
|
|
yarn dev
|
|
# or
|
|
pnpm dev
|
|
```
|
|
|
|
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
|
|
|
## Learn More
|
|
|
|
To learn more about Next.js, take a look at the following resources:
|
|
|
|
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
|
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
|
|
|
## Deploy on Vercel
|
|
|
|
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new).
|
|
|
|
Check out the [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
|
"""
|
|
|
|
def _get_app_content(self) -> str:
|
|
"""Generate _app content for Pages Router."""
|
|
return """export default function App({ Component, pageProps }) {
|
|
return <Component {...pageProps} />
|
|
}
|
|
"""
|
|
|
|
def _get_index_content(self) -> str:
|
|
"""Generate index content for Pages Router."""
|
|
return """export default function Home() {
|
|
return (
|
|
<main>
|
|
<h1>Welcome to Next.js!</h1>
|
|
<p>Get started by editing this page.</p>
|
|
</main>
|
|
)
|
|
}
|
|
"""
|
|
|
|
def initialize(self) -> None:
|
|
"""Run full initialization process."""
|
|
try:
|
|
print(f"Initializing Next.js project: {self.name}")
|
|
print(f"TypeScript: {self.typescript}")
|
|
print(f"App Router: {self.app_router}")
|
|
print(f"Tailwind CSS: {self.tailwind}")
|
|
print(f"ESLint: {self.eslint}")
|
|
print()
|
|
|
|
self.validate_name()
|
|
self.check_directory()
|
|
self.create_directory_structure()
|
|
self.create_config_files()
|
|
|
|
print()
|
|
print(f"✓ Project initialized successfully!")
|
|
print()
|
|
print(f"Next steps:")
|
|
print(f" cd {self.name}")
|
|
print(f" npm install")
|
|
print(f" npm run dev")
|
|
print()
|
|
|
|
except Exception as e:
|
|
print(f"Error: {e}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
|
|
def main():
|
|
"""CLI entry point."""
|
|
parser = argparse.ArgumentParser(
|
|
description="Initialize Next.js project with best practices"
|
|
)
|
|
parser.add_argument("name", help="Project name")
|
|
parser.add_argument(
|
|
"--directory", type=Path, help="Target directory (default: ./<name>)"
|
|
)
|
|
parser.add_argument(
|
|
"--no-typescript", action="store_true", help="Disable TypeScript"
|
|
)
|
|
parser.add_argument(
|
|
"--pages-router", action="store_true", help="Use Pages Router instead of App Router"
|
|
)
|
|
parser.add_argument("--src-dir", action="store_true", help="Use src/ directory")
|
|
parser.add_argument("--tailwind", action="store_true", help="Include Tailwind CSS")
|
|
parser.add_argument("--no-eslint", action="store_true", help="Disable ESLint")
|
|
parser.add_argument(
|
|
"--import-alias", default="@/*", help="Import alias pattern (default: @/*)"
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
initializer = NextJSInitializer(
|
|
name=args.name,
|
|
directory=args.directory,
|
|
typescript=not args.no_typescript,
|
|
app_router=not args.pages_router,
|
|
src_dir=args.src_dir,
|
|
tailwind=args.tailwind,
|
|
eslint=not args.no_eslint,
|
|
import_alias=args.import_alias,
|
|
)
|
|
|
|
initializer.initialize()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|