Files
2025-11-30 08:48:52 +08:00

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()