web-frameworks
Build modern full-stack web applications with Next.js (App Router, Server Components, RSC, PPR, SSR, SSG, ISR), Turborepo (monorepo management, task pipelines, remote caching, parallel execution), and RemixIcon (3100+ SVG icons in outlined/filled styles). Use when creating React
Install
npx skills add https://github.com/VoDaiLocz/kilo-kit-mcp/tree/main/skills/engineering/web-frameworks
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install vodailocz-kilo-kit-mcp@llmmart
git clone https://github.com/VoDaiLocz/kilo-kit-mcp.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole vodailocz/kilo-kit-mcp collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Web Frameworks Skill Group
Comprehensive guide for building modern full-stack web applications using Next.js, Turborepo, and RemixIcon.
Overview
This skill group combines three powerful tools for web development:
Next.js - React framework with SSR, SSG, RSC, and optimization features Turborepo - High-performance monorepo build system for JavaScript/TypeScript RemixIcon - Icon library with 3,100+ outlined and filled style icons
When to Use This Skill Group
- Building new full-stack web applications with modern React
- Setting up monorepos with multiple apps and shared packages
- Implementing server-side rendering and static generation
- Optimizing build performance with intelligent caching
- Creating consistent UI with professional iconography
- Managing workspace dependencies across multiple projects
- Deploying production-ready applications with proper optimization
Stack Selection Guide
Single Application: Next.js + RemixIcon
Use when building a standalone application:
- E-commerce sites
- Marketing websites
- SaaS applications
- Documentation sites
- Blogs and content platforms
Setup:
npx create-next-app@latest my-app
cd my-app
npm install remixicon
Monorepo: Next.js + Turborepo + RemixIcon
Use when building multiple applications with shared code:
- Microfrontends
- Multi-tenant platforms
- Internal tools with shared component library
- Multiple apps (web, admin, mobile-web) sharing logic
- Design system with documentation site
Setup:
npx create-turbo@latest my-monorepo
# Then configure Next.js apps in apps/ directory
# Install remixicon in shared UI packages
Framework Features Comparison
| Feature | Next.js | Turborepo | RemixIcon |
|---|---|---|---|
| Primary Use | Web framework | Build system | UI icons |
| Best For | SSR/SSG apps | Monorepos | Consistent iconography |
| Performance | Built-in optimization | Caching & parallel tasks | Lightweight fonts/SVG |
| TypeScript | Full support | Full support | Type definitions available |
Quick Start
Next.js Application
# Create new project
npx create-next-app@latest my-app
cd my-app
# Install RemixIcon
npm install remixicon
# Import in layout
# app/layout.tsx
import 'remixicon/fonts/remixicon.css'
# Start development
npm run dev
Turborepo Monorepo
# Create monorepo
npx create-turbo@latest my-monorepo
cd my-monorepo
# Structure:
# apps/web/ - Next.js application
# apps/docs/ - Documentation site
# packages/ui/ - Shared components with RemixIcon
# packages/config/ - Shared configs
# turbo.json - Pipeline configuration
# Run all apps
npm run dev
# Build all packages
npm run build
RemixIcon Integration
// Webfont (HTML/CSS)
<i className="ri-home-line"></i>
<i className="ri-search-fill ri-2x"></i>
// React component
import { RiHomeLine, RiSearchFill } from "@remixicon/react"
<RiHomeLine size={24} />
<RiSearchFill size={32} color="blue" />
Reference Navigation
Next.js References:
- App Router Architecture - Routing, layouts, pages, parallel routes
- Server Components - RSC patterns, client vs server, streaming
- Data Fetching - fetch API, caching, revalidation, loading states
- Optimization - Images, fonts, scripts, bundle analysis, PPR
Turborepo References:
- Setup & Configuration - Installation, workspace config, package structure
- Task Pipelines - Dependencies, parallel execution, task ordering
- Caching Strategies - Local cache, remote cache, cache invalidation
RemixIcon References:
- Integration Guide - Installation, usage, customization, accessibility
Common Patterns & Workflows
Pattern 1: Full-Stack Monorepo
my-monorepo/
├── apps/
│ ├── web/ # Customer-facing Next.js app
│ ├── admin/ # Admin dashboard Next.js app
│ └── docs/ # Documentation site
├── packages/
│ ├── ui/ # Shared UI with RemixIcon
│ ├── api-client/ # API client library
│ ├── config/ # ESLint, TypeScript configs
│ └── types/ # Shared TypeScript types
└── turbo.json # Build pipeline
turbo.json:
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "!.next/cache/**", "dist/**"]
},
"dev": {
"cache": false,
"persistent": true
},
"lint": {},
"test": {
"dependsOn": ["build"]
}
}
}
Pattern 2: Shared Component Library
// packages/ui/src/button.tsx
import { RiLoader4Line } from "@remixicon/react"
export function Button({ children, loading, icon }) {
return (
<button>
{loading ? <RiLoader4Line className="animate-spin" /> : icon}
{children}
</button>
)
}
// apps/web/app/page.tsx
import { Button } from "@repo/ui/button"
import { RiHomeLine } from "@remixicon/react"
export default function Page() {
return <Button icon={<RiHomeLine />}>Home</Button>
}
Pattern 3: Optimized Data Fetching
// app/posts/[slug]/page.tsx
import { notFound } from 'next/navigation'
// Static generation at build time
export async function generateStaticParams() {
const posts = await getPosts()
return posts.map(post => ({ slug: post.slug }))
}
// Revalidate every hour
async function getPost(slug: string) {
const res = await fetch(`https://api.example.com/posts/${slug}`, {
next: { revalidate: 3600 }
})
if (!res.ok) return null
return res.json()
}
export default async function Post({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug)
if (!post) notFound()
return <article>{post.content}</article>
}
Pattern 4: Monorepo CI/CD Pipeline
# .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: 18
- run: npm install
- run: npx turbo run build test lint
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
Utility Scripts
Python utilities in scripts/ directory:
nextjs-init.py - Initialize Next.js project with best practices turborepo-migrate.py - Convert existing monorepo to Turborepo
Usage examples:
# Initialize new Next.js app with TypeScript and recommended setup
python scripts/nextjs-init.py --name my-app --typescript --app-router
# Migrate existing monorepo to Turborepo with dry-run
python scripts/turborepo-migrate.py --path ./my-monorepo --dry-run
# Run tests
cd scripts/tests
pytest
Best Practices
Next.js:
- Default to Server Components, use Client Components only when needed
- Implement proper loading and error states
- Use Image component for automatic optimization
- Set proper metadata for SEO
- Leverage caching strategies (force-cache, revalidate, no-store)
Turborepo:
- Structure monorepo with clear separation (apps/, packages/)
- Define task dependencies correctly (^build for topological)
- Configure outputs for proper caching
- Enable remote caching for team collaboration
- Use filters to run tasks on changed packages only
RemixIcon:
- Use line style for minimal interfaces, fill for emphasis
- Maintain 24x24 grid alignment for crisp rendering
- Provide aria-labels for accessibility
- Use currentColor for flexible theming
- Prefer webfonts for multiple icons, SVG for single icons
Resources
- Next.js: https://nextjs.org/docs/llms.txt
- Turborepo: https://turbo.build/repo/docs
- RemixIcon: https://remixicon.com
Implementation Checklist
Building with this stack:
- Create project structure (single app or monorepo)
- Configure TypeScript and ESLint
- Set up Next.js with App Router
- Configure Turborepo pipeline (if monorepo)
- Install and configure RemixIcon
- Implement routing and layouts
- Add loading and error states
- Configure image and font optimization
- Set up data fetching patterns
- Configure caching strategies
- Add API routes as needed
- Implement shared component library (if monorepo)
- Configure remote caching (if monorepo)
- Set up CI/CD pipeline
- Configure deployment platform
Files (kilo-kit-mcp)
-
references
-
nextjs-app-router.md 9.1 KB
# Next.js App Router Architecture Modern file-system based routing with React Server Components support. ## File Conventions Special files define route behavior: - `page.tsx` - Page UI, makes route publicly accessible - `layout.tsx` - Shared UI wrapper for segment and children - `loading.tsx` - Loading UI, automatically wraps page in Suspense - `error.tsx` - Error UI, wraps page in Error Boundary - `not-found.tsx` - 404 UI for route segment - `route.ts` - API endpoint (Route Handler) - `template.tsx` - Re-rendered layout (doesn't preserve state) - `default.tsx` - Fallback for parallel routes ## Basic Routing ### Static Routes ``` app/ ├── page.tsx → / ├── about/ │ └── page.tsx → /about ├── blog/ │ └── page.tsx → /blog └── contact/ └── page.tsx → /contact ``` ### Dynamic Routes Single parameter: ```tsx // app/blog/[slug]/page.tsx export default function BlogPost({ params }: { params: { slug: string } }) { return <h1>Post: {params.slug}</h1> } // Matches: /blog/hello-world, /blog/my-post ``` Catch-all segments: ```tsx // app/shop/[...slug]/page.tsx export default function Shop({ params }: { params: { slug: string[] } }) { return <h1>Category: {params.slug.join('/')}</h1> } // Matches: /shop/clothes, /shop/clothes/shirts, /shop/clothes/shirts/red ``` Optional catch-all: ```tsx // app/docs/[[...slug]]/page.tsx // Matches: /docs, /docs/getting-started, /docs/api/reference ``` ## Layouts ### Root Layout (Required) Must include `<html>` and `<body>` tags: ```tsx // app/layout.tsx export default function RootLayout({ children, }: { children: React.ReactNode }) { return ( <html lang="en"> <body> <header>Global Header</header> {children} <footer>Global Footer</footer> </body> </html> ) } ``` ### Nested Layouts ```tsx // app/dashboard/layout.tsx export default function DashboardLayout({ children, }: { children: React.ReactNode }) { return ( <div> <nav>Dashboard Navigation</nav> <main>{children}</main> </div> ) } ``` Layout characteristics: - Preserve state during navigation - Do not re-render on navigation between child routes - Can fetch data - Cannot access pathname or searchParams (use Client Component) ## Route Groups Organize routes without affecting URL structure: ``` app/ ├── (marketing)/ # Group without URL segment │ ├── about/page.tsx → /about │ ├── blog/page.tsx → /blog │ └── layout.tsx # Marketing layout ├── (shop)/ │ ├── products/page.tsx → /products │ ├── cart/page.tsx → /cart │ └── layout.tsx # Shop layout └── layout.tsx # Root layout ``` Use cases: - Multiple root layouts - Organize code by feature/team - Different layouts for different sections ## Parallel Routes Render multiple pages simultaneously in same layout: ``` app/ ├── @team/ # Named slot │ └── page.tsx ├── @analytics/ # Named slot │ └── page.tsx ├── page.tsx # Default children └── layout.tsx # Consumes slots ``` ```tsx // app/layout.tsx export default function Layout({ children, team, analytics, }: { children: React.ReactNode team: React.ReactNode analytics: React.ReactNode }) { return ( <> {children} <div className="grid grid-cols-2"> {team} {analytics} </div> </> ) } ``` Use cases: - Split views (dashboards) - Modals - Conditional rendering based on auth state ## Intercepting Routes Intercept navigation to show content in different context: ``` app/ ├── feed/ │ └── page.tsx ├── photo/ │ └── [id]/ │ └── page.tsx # Full photo page └── (..)photo/ # Intercepts /photo/[id] └── [id]/ └── page.tsx # Modal photo view ``` Matching conventions: - `(.)` - Match same level - `(..)` - Match one level above - `(..)(..)` - Match two levels above - `(...)` - Match from app root Use case: Display modal when navigating from feed, show full page when URL accessed directly ## Loading States ### Loading File Automatically wraps page in Suspense: ```tsx // app/dashboard/loading.tsx export default function Loading() { return <div className="spinner">Loading dashboard...</div> } ``` ### Manual Suspense Fine-grained control: ```tsx // app/page.tsx import { Suspense } from 'react' async function Posts() { const posts = await fetchPosts() return <PostsList posts={posts} /> } export default function Page() { return ( <div> <h1>My Blog</h1> <Suspense fallback={<div>Loading posts...</div>}> <Posts /> </Suspense> </div> ) } ``` ## Error Handling ### Error File Wraps segment in Error Boundary: ```tsx // app/error.tsx 'use client' // Error components must be Client Components export default function Error({ error, reset, }: { error: Error & { digest?: string } reset: () => void }) { return ( <div> <h2>Something went wrong!</h2> <p>{error.message}</p> <button onClick={() => reset()}>Try again</button> </div> ) } ``` ### Global Error Catches errors in root layout: ```tsx // app/global-error.tsx 'use client' export default function GlobalError({ error, reset, }: { error: Error & { digest?: string } reset: () => void }) { return ( <html> <body> <h2>Application Error!</h2> <button onClick={() => reset()}>Try again</button> </body> </html> ) } ``` ### Not Found ```tsx // app/blog/[slug]/page.tsx import { notFound } from 'next/navigation' export default async function Post({ params }: { params: { slug: string } }) { const post = await getPost(params.slug) if (!post) { notFound() // Triggers not-found.tsx } return <article>{post.content}</article> } // app/blog/[slug]/not-found.tsx export default function NotFound() { return <h2>Post not found</h2> } ``` ## Navigation ### Link Component ```tsx import Link from 'next/link' // Basic link <Link href="/about">About</Link> // Dynamic route <Link href={`/blog/${post.slug}`}>Read Post</Link> // With object <Link href={{ pathname: '/blog/[slug]', query: { slug: 'hello-world' }, }}> Read Post </Link> // Prefetch control <Link href="/dashboard" prefetch={false}> Dashboard </Link> // Replace history <Link href="/search" replace> Search </Link> ``` ### useRouter Hook (Client) ```tsx 'use client' import { useRouter } from 'next/navigation' export function NavigateButton() { const router = useRouter() return ( <> <button onClick={() => router.push('/dashboard')}>Dashboard</button> <button onClick={() => router.replace('/login')}>Login</button> <button onClick={() => router.refresh()}>Refresh</button> <button onClick={() => router.back()}>Back</button> <button onClick={() => router.forward()}>Forward</button> </> ) } ``` ### Programmatic Navigation (Server) ```tsx import { redirect } from 'next/navigation' export default async function Page() { const session = await getSession() if (!session) { redirect('/login') } return <div>Protected content</div> } ``` ## Accessing Route Information ### searchParams (Server) ```tsx // app/shop/page.tsx export default function Shop({ searchParams, }: { searchParams: { sort?: string; filter?: string } }) { const sort = searchParams.sort || 'newest' const filter = searchParams.filter return <div>Showing: {filter}, sorted by {sort}</div> } // Accessed via: /shop?sort=price&filter=shirts ``` ### useSearchParams (Client) ```tsx 'use client' import { useSearchParams } from 'next/navigation' export function SearchFilter() { const searchParams = useSearchParams() const query = searchParams.get('q') return <div>Search query: {query}</div> } ``` ### usePathname (Client) ```tsx 'use client' import { usePathname } from 'next/navigation' import Link from 'next/link' export function Navigation() { const pathname = usePathname() return ( <nav> <Link href="/" className={pathname === '/' ? 'active' : ''}> Home </Link> <Link href="/about" className={pathname === '/about' ? 'active' : ''}> About </Link> </nav> ) } ``` ## Project Structure Best Practices ``` app/ ├── (auth)/ # Route group for auth pages │ ├── login/ │ ├── signup/ │ └── layout.tsx # Auth layout ├── (dashboard)/ # Route group for dashboard │ ├── dashboard/ │ ├── settings/ │ └── layout.tsx # Dashboard layout ├── api/ # API routes │ ├── auth/ │ └── posts/ ├── _components/ # Private folder (not routes) │ ├── header.tsx │ └── footer.tsx ├── _lib/ # Private utilities │ ├── auth.ts │ └── db.ts ├── layout.tsx # Root layout ├── page.tsx # Home page ├── loading.tsx ├── error.tsx └── not-found.tsx ``` Use underscore prefix for folders that shouldn't be routes. -
nextjs-data-fetching.md 10 KB
# Next.js Data Fetching Server-side data fetching, caching strategies, revalidation, and loading patterns. ## Fetch API Extensions Next.js extends native fetch with caching and revalidation: ```tsx // Force cache (default) - cache forever fetch('https://api.example.com/data', { cache: 'force-cache' }) // No cache - fetch on every request fetch('https://api.example.com/data', { cache: 'no-store' }) // Revalidate - cache with time-based revalidation fetch('https://api.example.com/data', { next: { revalidate: 3600 } }) // Tag-based revalidation fetch('https://api.example.com/data', { next: { tags: ['posts'] } }) ``` ## Caching Strategies ### Static Data (Default) Fetched at build time, cached indefinitely: ```tsx // app/posts/page.tsx async function getPosts() { const res = await fetch('https://api.example.com/posts') // Same as: fetch(url, { cache: 'force-cache' }) return res.json() } export default async function Posts() { const posts = await getPosts() return <PostsList posts={posts} /> } ``` Use for: Content that rarely changes, static pages ### Dynamic Data Fetched on every request: ```tsx async function getUser() { const res = await fetch('https://api.example.com/user', { cache: 'no-store' }) return res.json() } export default async function Profile() { const user = await getUser() return <div>{user.name}</div> } ``` Use for: User-specific data, real-time content ### Incremental Static Regeneration (ISR) Revalidate cached data after time period: ```tsx async function getPosts() { const res = await fetch('https://api.example.com/posts', { next: { revalidate: 60 } // Revalidate every 60 seconds }) return res.json() } export default async function Posts() { const posts = await getPosts() return <PostsList posts={posts} /> } ``` How it works: 1. First request: Generate page, cache it 2. Subsequent requests: Serve cached page 3. After 60s: Next request triggers regeneration in background 4. New page cached, served to subsequent requests Use for: News sites, blogs, product listings ## Revalidation Strategies ### Time-Based Revalidation ```tsx // Revalidate every hour fetch('https://api.example.com/posts', { next: { revalidate: 3600 } }) // Revalidate every 10 seconds fetch('https://api.example.com/trending', { next: { revalidate: 10 } }) ``` ### On-Demand Revalidation Revalidate specific paths or tags programmatically: ```tsx // app/actions.ts 'use server' import { revalidatePath, revalidateTag } from 'next/cache' export async function createPost(formData: FormData) { const post = await db.post.create({ data: { title: formData.get('title'), content: formData.get('content') } }) // Revalidate specific path revalidatePath('/posts') revalidatePath(`/posts/${post.id}`) // Or revalidate by tag revalidateTag('posts') } ``` Tag-based revalidation: ```tsx // Fetch with tags async function getPosts() { const res = await fetch('https://api.example.com/posts', { next: { tags: ['posts', 'content'] } }) return res.json() } async function getComments(postId: string) { const res = await fetch(`https://api.example.com/comments/${postId}`, { next: { tags: ['comments', `post-${postId}`] } }) return res.json() } // Revalidate all 'posts' tagged requests revalidateTag('posts') // Revalidate specific post comments revalidateTag(`post-${postId}`) ``` ### Route Segment Config Configure entire route segment: ```tsx // app/posts/page.tsx export const revalidate = 3600 // Revalidate every hour export default async function Posts() { const posts = await fetch('https://api.example.com/posts').then(r => r.json()) return <PostsList posts={posts} /> } ``` Options: ```tsx export const dynamic = 'auto' // default export const dynamic = 'force-dynamic' // no caching export const dynamic = 'error' // error if dynamic export const dynamic = 'force-static' // force static export const revalidate = false // never revalidate (default) export const revalidate = 0 // no cache export const revalidate = 60 // revalidate every 60s export const fetchCache = 'auto' // default export const fetchCache = 'default-cache' export const fetchCache = 'only-cache' export const fetchCache = 'force-cache' export const fetchCache = 'default-no-store' export const fetchCache = 'only-no-store' export const fetchCache = 'force-no-store' ``` ## Data Fetching Patterns ### Parallel Fetching Fetch multiple resources simultaneously: ```tsx async function getData() { // Start both requests in parallel const [posts, users] = await Promise.all([ fetch('https://api.example.com/posts').then(r => r.json()), fetch('https://api.example.com/users').then(r => r.json()) ]) return { posts, users } } export default async function Page() { const { posts, users } = await getData() return ( <div> <PostsList posts={posts} /> <UsersList users={users} /> </div> ) } ``` ### Sequential Fetching Fetch dependent data: ```tsx async function getData(postId: string) { // Fetch post first const post = await fetch(`https://api.example.com/posts/${postId}`).then(r => r.json()) // Then fetch author based on post data const author = await fetch(`https://api.example.com/users/${post.authorId}`).then(r => r.json()) return { post, author } } export default async function Post({ params }: { params: { id: string } }) { const { post, author } = await getData(params.id) return ( <article> <h1>{post.title}</h1> <p>By {author.name}</p> <div>{post.content}</div> </article> ) } ``` ### Preloading Data Optimize sequential waterfalls: ```tsx // lib/data.ts import { cache } from 'react' export const getUser = cache(async (id: string) => { const res = await fetch(`https://api.example.com/users/${id}`) return res.json() }) // app/user/[id]/page.tsx import { getUser } from '@/lib/data' // Preload before component renders async function preload(id: string) { void getUser(id) // Start fetching immediately } export default async function User({ params }: { params: { id: string } }) { preload(params.id) // Start fetch // Render other UI const user = await getUser(params.id) // Will use cached result return <div>{user.name}</div> } ``` ## Loading States ### Loading File Automatic loading UI: ```tsx // app/dashboard/loading.tsx export default function Loading() { return <div className="spinner">Loading dashboard...</div> } // app/dashboard/page.tsx export default async function Dashboard() { const data = await fetchDashboard() return <DashboardView data={data} /> } ``` ### Suspense Boundaries Granular loading states: ```tsx // app/dashboard/page.tsx import { Suspense } from 'react' async function Revenue() { const data = await fetchRevenue() // 2s return <RevenueChart data={data} /> } async function Sales() { const data = await fetchSales() // 0.5s return <SalesTable data={data} /> } export default function Dashboard() { return ( <div> <h1>Dashboard</h1> <Suspense fallback={<RevenueChartSkeleton />}> <Revenue /> </Suspense> <Suspense fallback={<SalesTableSkeleton />}> <Sales /> </Suspense> </div> ) } ``` Sales loads after 0.5s, Revenue after 2s - no blocking. ## Static Generation ### generateStaticParams Pre-render dynamic routes at build time: ```tsx // app/posts/[slug]/page.tsx export async function generateStaticParams() { const posts = await fetch('https://api.example.com/posts').then(r => r.json()) return posts.map(post => ({ slug: post.slug })) } export default async function Post({ params }: { params: { slug: string } }) { const post = await fetch(`https://api.example.com/posts/${params.slug}`).then(r => r.json()) return <article>{post.content}</article> } ``` Generates static pages at build: - `/posts/hello-world` - `/posts/nextjs-guide` - `/posts/react-tips` ### Dynamic Params Handling ```tsx // app/posts/[slug]/page.tsx export const dynamicParams = true // default - generate on-demand if not pre-rendered export const dynamicParams = false // 404 for paths not in generateStaticParams ``` ## Error Handling ### Try-Catch in Server Components ```tsx async function getData() { try { const res = await fetch('https://api.example.com/data') if (!res.ok) { throw new Error('Failed to fetch data') } return res.json() } catch (error) { console.error('Data fetch error:', error) return null } } export default async function Page() { const data = await getData() if (!data) { return <div>Failed to load data</div> } return <DataView data={data} /> } ``` ### Error Boundaries ```tsx // app/error.tsx 'use client' export default function Error({ error, reset, }: { error: Error & { digest?: string } reset: () => void }) { return ( <div> <h2>Something went wrong!</h2> <button onClick={() => reset()}>Try again</button> </div> ) } ``` ## Database Queries Direct database access in Server Components: ```tsx // lib/db.ts import { PrismaClient } from '@prisma/client' const prisma = new PrismaClient() export async function getPosts() { return prisma.post.findMany({ include: { author: true }, orderBy: { createdAt: 'desc' } }) } // app/posts/page.tsx import { getPosts } from '@/lib/db' export default async function Posts() { const posts = await getPosts() return <PostsList posts={posts} /> } ``` ## Best Practices 1. **Default to static** - Use `cache: 'force-cache'` or default behavior 2. **Use ISR for semi-dynamic content** - Balance freshness and performance 3. **Fetch in parallel** - Use `Promise.all()` for independent requests 4. **Add loading states** - Use Suspense for better UX 5. **Handle errors gracefully** - Provide fallbacks and error boundaries 6. **Use on-demand revalidation** - Trigger updates after mutations 7. **Tag your fetches** - Enable granular cache invalidation 8. **Dedupe automatically** - Next.js dedupes identical fetch requests 9. **Avoid client-side fetching** - Use Server Components when possible 10. **Cache database queries** - Use React cache() for expensive queries -
nextjs-optimization.md 10.9 KB
# Next.js Optimization Performance optimization techniques for images, fonts, scripts, and bundles. ## Image Optimization ### Next.js Image Component Automatic optimization with modern formats (WebP, AVIF): ```tsx import Image from 'next/image' export default function Page() { return ( <> {/* Local image */} <Image src="/hero.jpg" alt="Hero image" width={1200} height={600} priority // Load immediately, no lazy loading /> {/* Remote image */} <Image src="https://example.com/photo.jpg" alt="Photo" width={800} height={600} quality={90} // 1-100, default 75 /> {/* Responsive fill */} <div style={{ position: 'relative', width: '100%', height: '400px' }}> <Image src="/background.jpg" alt="Background" fill style={{ objectFit: 'cover' }} sizes="100vw" /> </div> {/* With blur placeholder */} <Image src="/profile.jpg" alt="Profile" width={200} height={200} placeholder="blur" blurDataURL="data:image/jpeg;base64,..." // Or use static import /> </> ) } ``` ### Image Props Reference **Required:** - `src` - Image path (string or static import) - `alt` - Alt text for accessibility - `width`, `height` - Dimensions (required unless using `fill`) **Optional:** - `fill` - Fill parent container (makes width/height optional) - `sizes` - Responsive sizes hint for srcset - `quality` - 1-100 (default 75) - `priority` - Disable lazy loading, preload image - `placeholder` - 'blur' | 'empty' (default 'empty') - `blurDataURL` - Data URL for blur placeholder - `loading` - 'lazy' | 'eager' (default 'lazy') - `style` - CSS styles - `className` - CSS class - `onLoad` - Callback when loaded ### Responsive Images with Sizes ```tsx <Image src="/hero.jpg" alt="Hero" fill sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw" /> ``` This tells browser: - Mobile (<768px): Use 100% viewport width - Tablet (768-1200px): Use 50% viewport width - Desktop (>1200px): Use 33% viewport width ### Static Import for Local Images ```tsx import heroImage from '@/public/hero.jpg' <Image src={heroImage} alt="Hero" placeholder="blur" // Automatically generated // No width/height needed - inferred from import /> ``` ### Remote Image Configuration ```js // next.config.js module.exports = { images: { remotePatterns: [ { protocol: 'https', hostname: 'example.com', pathname: '/images/**', }, { protocol: 'https', hostname: 'cdn.example.com', } ], // Device sizes for srcset deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840], // Image sizes for srcset imageSizes: [16, 32, 48, 64, 96, 128, 256, 384], // Supported formats formats: ['image/webp'], // Cache optimization images for 60 days minimumCacheTTL: 60 * 60 * 24 * 60, } } ``` ## Font Optimization ### Google Fonts Automatic optimization with zero layout shift: ```tsx // app/layout.tsx import { Inter, Roboto_Mono, Playfair_Display } from 'next/font/google' const inter = Inter({ subsets: ['latin'], display: 'swap', variable: '--font-inter', }) const robotoMono = Roboto_Mono({ subsets: ['latin'], display: 'swap', weight: ['400', '700'], variable: '--font-roboto-mono', }) const playfair = Playfair_Display({ subsets: ['latin'], display: 'swap', weight: ['400', '700', '900'], style: ['normal', 'italic'], }) export default function RootLayout({ children }) { return ( <html lang="en" className={`${inter.variable} ${robotoMono.variable}`}> <body className={inter.className}>{children}</body> </html> ) } ``` Use CSS variables: ```css .code { font-family: var(--font-roboto-mono); } ``` ### Local Fonts ```tsx import localFont from 'next/font/local' const myFont = localFont({ src: [ { path: './fonts/my-font-regular.woff2', weight: '400', style: 'normal', }, { path: './fonts/my-font-bold.woff2', weight: '700', style: 'normal', } ], variable: '--font-my-font', display: 'swap', }) export default function RootLayout({ children }) { return ( <html lang="en" className={myFont.variable}> <body>{children}</body> </html> ) } ``` ### Font Display Strategies ```tsx const font = Inter({ display: 'swap', // Show fallback immediately, swap when loaded (recommended) // display: 'optional', // Only use font if available immediately // display: 'block', // Hide text until font loads (max 3s) // display: 'fallback', // Show fallback briefly, swap if loaded quickly // display: 'auto', // Browser default }) ``` ## Script Optimization ### Script Component Control loading behavior: ```tsx import Script from 'next/script' export default function Page() { return ( <> {/* Load after page is interactive (recommended for analytics) */} <Script src="https://www.googletagmanager.com/gtag/js" strategy="afterInteractive" /> {/* Load while page is idle (lowest priority) */} <Script src="https://connect.facebook.net/en_US/sdk.js" strategy="lazyOnload" /> {/* Load before page is interactive (use sparingly) */} <Script src="https://maps.googleapis.com/maps/api/js" strategy="beforeInteractive" /> {/* Inline script with strategy */} <Script id="analytics" strategy="afterInteractive"> {` window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); `} </Script> {/* With onLoad callback */} <Script src="https://example.com/sdk.js" onLoad={() => console.log('Script loaded')} onError={(e) => console.error('Script failed', e)} /> </> ) } ``` **Strategy options:** - `beforeInteractive` - Load before page interactive (blocking) - `afterInteractive` - Load after page interactive (default) - `lazyOnload` - Load during idle time - `worker` - Load in web worker (experimental) ## Bundle Optimization ### Analyzing Bundle Size ```bash # Install bundle analyzer npm install @next/bundle-analyzer # Create next.config.js wrapper ``` ```js // next.config.js const withBundleAnalyzer = require('@next/bundle-analyzer')({ enabled: process.env.ANALYZE === 'true', }) module.exports = withBundleAnalyzer({ // Your Next.js config }) ``` ```bash # Run analysis ANALYZE=true npm run build ``` ### Dynamic Import (Code Splitting) Split code and load on-demand: ```tsx import dynamic from 'next/dynamic' // Dynamic import with loading state const DynamicChart = dynamic(() => import('@/components/chart'), { loading: () => <div>Loading chart...</div>, ssr: false, // Disable SSR for this component }) export default function Dashboard() { return ( <div> <h1>Dashboard</h1> <DynamicChart /> </div> ) } ``` Named exports: ```tsx const DynamicComponent = dynamic( () => import('@/components/hello').then(mod => mod.Hello) ) ``` Multiple components: ```tsx const DynamicHeader = dynamic(() => import('@/components/header')) const DynamicFooter = dynamic(() => import('@/components/footer')) ``` ### Tree Shaking Import only what you need: ```tsx // ❌ Bad - imports entire library import _ from 'lodash' const result = _.debounce(fn, 300) // ✅ Good - imports only debounce import debounce from 'lodash/debounce' const result = debounce(fn, 300) // ❌ Bad import * as Icons from 'react-icons/fa' <Icons.FaHome /> // ✅ Good import { FaHome } from 'react-icons/fa' <FaHome /> ``` ## Partial Prerendering (PPR) Experimental: Combine static and dynamic rendering in same route. ```js // next.config.js module.exports = { experimental: { ppr: true, } } ``` ```tsx // app/page.tsx import { Suspense } from 'react' // Static shell export default function Page() { return ( <div> <header>Static Header</header> {/* Dynamic content with Suspense boundary */} <Suspense fallback={<div>Loading...</div>}> <DynamicContent /> </Suspense> <footer>Static Footer</footer> </div> ) } // Dynamic component async function DynamicContent() { const data = await fetch('https://api.example.com/data', { cache: 'no-store' }).then(r => r.json()) return <div>{data.content}</div> } ``` Static shell loads instantly, dynamic content streams in. ## Metadata Optimization ### Static Metadata ```tsx // app/page.tsx import { Metadata } from 'next' export const metadata: Metadata = { title: 'My Page', description: 'Page description', keywords: ['next.js', 'react', 'javascript'], openGraph: { title: 'My Page', description: 'Page description', images: ['/og-image.jpg'], }, twitter: { card: 'summary_large_image', title: 'My Page', description: 'Page description', images: ['/twitter-image.jpg'], }, alternates: { canonical: 'https://example.com/page', }, robots: { index: true, follow: true, }, } ``` ### Dynamic Metadata ```tsx // app/blog/[slug]/page.tsx export async function generateMetadata({ params }): Promise<Metadata> { const post = await getPost(params.slug) return { title: post.title, description: post.excerpt, openGraph: { title: post.title, description: post.excerpt, images: [post.coverImage], type: 'article', publishedTime: post.publishedAt, authors: [post.author.name], }, } } ``` ### Metadata Files Create these files in `app/` directory: - `favicon.ico` - Favicon - `icon.png` / `icon.jpg` - App icon - `apple-icon.png` - Apple touch icon - `opengraph-image.png` - Open Graph image - `twitter-image.png` - Twitter card image - `robots.txt` - Robots file - `sitemap.xml` - Sitemap Or generate dynamically: ```tsx // app/sitemap.ts export default async function sitemap() { const posts = await getPosts() return [ { url: 'https://example.com', lastModified: new Date(), }, ...posts.map(post => ({ url: `https://example.com/blog/${post.slug}`, lastModified: post.updatedAt, })) ] } ``` ## Performance Best Practices 1. **Use Image component** - Automatic optimization, lazy loading, modern formats 2. **Optimize fonts** - Use next/font to eliminate layout shift 3. **Dynamic imports** - Code split large components and third-party libraries 4. **Analyze bundle** - Identify and eliminate large dependencies 5. **Proper caching** - Use ISR for semi-static content 6. **Streaming with Suspense** - Load fast content first, stream slow content 7. **Minimize JavaScript** - Default to Server Components 8. **Prefetch links** - Next.js prefetches Link components in viewport 9. **Use Script component** - Control third-party script loading 10. **Compress assets** - Enable compression in hosting platform 11. **Use CDN** - Deploy to edge network (Vercel, Cloudflare) 12. **Monitor metrics** - Track Core Web Vitals (LCP, FID, CLS) -
nextjs-server-components.md 10.5 KB
# Next.js Server Components React Server Components (RSC) architecture, patterns, and best practices. ## Core Concepts ### Server Components (Default) All components in `app/` directory are Server Components by default: ```tsx // app/posts/page.tsx - Server Component async function getPosts() { const res = await fetch('https://api.example.com/posts') return res.json() } export default async function PostsPage() { const posts = await getPosts() return ( <div> {posts.map(post => ( <article key={post.id}>{post.title}</article> ))} </div> ) } ``` **Benefits:** - Fetch data on server (direct database access) - Keep sensitive data/keys on server - Reduce client-side JavaScript bundle - Improve initial page load and SEO - Cache results on server - Stream content to client **Limitations:** - Cannot use React hooks (useState, useEffect, useContext) - Cannot use browser APIs (window, localStorage) - Cannot add event listeners (onClick, onChange) - Cannot use React class components ### Client Components Mark with `'use client'` directive at top of file: ```tsx // components/counter.tsx - Client Component 'use client' import { useState } from 'react' export function Counter() { const [count, setCount] = useState(0) return ( <button onClick={() => setCount(count + 1)}> Count: {count} </button> ) } ``` **Use Client Components for:** - Interactive UI (event handlers) - State management (useState, useReducer) - Effects (useEffect, useLayoutEffect) - Browser-only APIs (localStorage, geolocation) - Custom React hooks - Context consumers ## Composition Patterns ### Server Component as Wrapper Best practice: Keep Server Components as parent, pass Client Components as children: ```tsx // app/page.tsx - Server Component import { ClientSidebar } from './sidebar' import { ClientButton } from './button' export default async function Page() { const data = await fetchData() // Server-side data fetch return ( <div> <h1>Server-rendered heading</h1> <ClientSidebar /> <ClientButton /> <p>More server-rendered content: {data.title}</p> </div> ) } ``` ### Passing Server Components to Client Components Use children pattern to avoid making entire tree client-side: ```tsx // app/page.tsx - Server Component import { ClientProvider } from './client-provider' import { ServerContent } from './server-content' export default function Page() { return ( <ClientProvider> <ServerContent /> {/* Stays as Server Component */} </ClientProvider> ) } // client-provider.tsx - Client Component 'use client' export function ClientProvider({ children }: { children: React.ReactNode }) { const [state, setState] = useState() return <div>{children}</div> } // server-content.tsx - Server Component export async function ServerContent() { const data = await fetchData() return <p>{data.content}</p> } ``` ### Sharing Data Between Server Components No need for props or context - just fetch data where needed: ```tsx // lib/data.ts export async function getUser() { const res = await fetch('https://api.example.com/user', { cache: 'force-cache' // Will dedupe automatically }) return res.json() } // app/header.tsx import { getUser } from '@/lib/data' export async function Header() { const user = await getUser() // Fetch 1 return <div>Welcome, {user.name}</div> } // app/profile.tsx import { getUser } from '@/lib/data' export async function Profile() { const user = await getUser() // Fetch 2 (deduped automatically) return <div>Email: {user.email}</div> } ``` Next.js automatically dedupes identical fetch requests during render. ## Async Components Server Components can be async functions: ```tsx // app/posts/[id]/page.tsx async function getPost(id: string) { const res = await fetch(`https://api.example.com/posts/${id}`) return res.json() } async function getComments(postId: string) { const res = await fetch(`https://api.example.com/posts/${postId}/comments`) return res.json() } export default async function Post({ params }: { params: { id: string } }) { // Parallel data fetching const [post, comments] = await Promise.all([ getPost(params.id), getComments(params.id) ]) return ( <article> <h1>{post.title}</h1> <p>{post.content}</p> <CommentsList comments={comments} /> </article> ) } ``` ## Streaming with Suspense Stream components as they resolve: ```tsx // app/page.tsx import { Suspense } from 'react' async function SlowComponent() { await new Promise(resolve => setTimeout(resolve, 3000)) return <div>Loaded after 3 seconds</div> } async function FastComponent() { await new Promise(resolve => setTimeout(resolve, 500)) return <div>Loaded after 0.5 seconds</div> } export default function Page() { return ( <div> <h1>Instant heading</h1> <Suspense fallback={<div>Loading fast...</div>}> <FastComponent /> </Suspense> <Suspense fallback={<div>Loading slow...</div>}> <SlowComponent /> </Suspense> </div> ) } ``` Benefits: - Fast components render immediately - Slow components don't block page - Progressive enhancement - Better perceived performance ## Context in Server/Client Components ### Problem: Context Requires Client Components ```tsx // ❌ Won't work - Server Components can't use context import { createContext } from 'react' const ThemeContext = createContext() export default function Layout({ children }) { return ( <ThemeContext.Provider value="dark"> {children} </ThemeContext.Provider> ) } ``` ### Solution: Create Client Component Wrapper ```tsx // app/providers.tsx - Client Component 'use client' import { createContext, useContext } from 'react' const ThemeContext = createContext('light') export function ThemeProvider({ children }: { children: React.ReactNode }) { return ( <ThemeContext.Provider value="dark"> {children} </ThemeContext.Provider> ) } export function useTheme() { return useContext(ThemeContext) } // app/layout.tsx - Server Component import { ThemeProvider } from './providers' export default function RootLayout({ children }) { return ( <html> <body> <ThemeProvider> {children} </ThemeProvider> </body> </html> ) } ``` ## Third-Party Component Integration Many third-party components need client-side features: ```tsx // components/carousel.tsx 'use client' import 'slick-carousel/slick/slick.css' import Slider from 'react-slick' export function Carousel({ children }) { return <Slider>{children}</Slider> } // app/page.tsx - Server Component import { Carousel } from '@/components/carousel' export default function Page() { return ( <Carousel> <div>Slide 1</div> <div>Slide 2</div> </Carousel> ) } ``` ## Server Actions Call server-side functions from Client Components: ```tsx // app/actions.ts 'use server' import { revalidatePath } from 'next/cache' import { db } from '@/lib/db' export async function createPost(formData: FormData) { const title = formData.get('title') as string const content = formData.get('content') as string await db.post.create({ data: { title, content } }) revalidatePath('/posts') } // app/new-post/page.tsx import { createPost } from '@/app/actions' export default function NewPost() { return ( <form action={createPost}> <input name="title" required /> <textarea name="content" required /> <button type="submit">Create Post</button> </form> ) } ``` With Client Component: ```tsx // components/post-form.tsx 'use client' import { createPost } from '@/app/actions' import { useFormStatus } from 'react-dom' function SubmitButton() { const { pending } = useFormStatus() return ( <button type="submit" disabled={pending}> {pending ? 'Creating...' : 'Create Post'} </button> ) } export function PostForm() { return ( <form action={createPost}> <input name="title" required /> <textarea name="content" required /> <SubmitButton /> </form> ) } ``` ## When to Use Each Component Type ### Use Server Components When: - Fetching data from database or API - Accessing backend resources directly - Keeping sensitive information on server (tokens, keys) - Reducing client-side JavaScript - Rendering static content - No interactivity needed ### Use Client Components When: - Adding interactivity (onClick, onChange) - Managing state (useState, useReducer) - Using lifecycle effects (useEffect) - Using browser-only APIs (localStorage, navigator) - Using custom React hooks - Using React Context - Using third-party libraries requiring client features ## Best Practices 1. **Default to Server Components** - Only use 'use client' when needed 2. **Move Client Components to leaves** - Keep them as deep as possible in tree 3. **Pass Server Components as children** - Avoid turning entire trees client-side 4. **Share data via fetch** - Let Next.js dedupe requests automatically 5. **Use Suspense for streaming** - Improve perceived performance 6. **Separate client logic** - Extract client-only code to separate files 7. **Minimize client bundle** - Less JavaScript = faster page loads ## Common Patterns ### Protected Content ```tsx // app/dashboard/page.tsx - Server Component import { redirect } from 'next/navigation' import { getUser } from '@/lib/auth' export default async function Dashboard() { const user = await getUser() if (!user) { redirect('/login') } return <div>Welcome, {user.name}</div> } ``` ### Optimistic Updates ```tsx // components/like-button.tsx 'use client' import { useOptimistic } from 'react' import { likePost } from '@/app/actions' export function LikeButton({ postId, initialLikes }) { const [optimisticLikes, addOptimisticLike] = useOptimistic( initialLikes, (state, amount) => state + amount ) return ( <button onClick={async () => { addOptimisticLike(1) await likePost(postId) }} > Likes: {optimisticLikes} </button> ) } ``` ### Loading States with Streaming ```tsx // app/dashboard/page.tsx import { Suspense } from 'react' async function RevenueChart() { const data = await fetchRevenue() // Slow query return <Chart data={data} /> } async function RecentSales() { const sales = await fetchSales() // Fast query return <SalesTable sales={sales} /> } export default function Dashboard() { return ( <div> <h1>Dashboard</h1> <Suspense fallback={<ChartSkeleton />}> <RevenueChart /> </Suspense> <Suspense fallback={<TableSkeleton />}> <RecentSales /> </Suspense> </div> ) } ``` -
remix-icon-integration.md 10.6 KB
# RemixIcon Integration Guide Installation, usage, customization, and accessibility for RemixIcon library. ## Overview RemixIcon provides 3,100+ icons in outlined (-line) and filled (-fill) styles, built on 24x24px grid. **Icon naming:** `ri-{name}-{style}` - Examples: `ri-home-line`, `ri-heart-fill`, `ri-search-line` ## Installation ### NPM Package ```bash # npm npm install remixicon # yarn yarn add remixicon # pnpm pnpm install remixicon # bun bun add remixicon ``` ### React Package ```bash npm install @remixicon/react ``` ### Vue 3 Package ```bash npm install @remixicon/vue ``` ### CDN ```html <link href="https://cdn.jsdelivr.net/npm/remixicon@4.7.0/fonts/remixicon.css" rel="stylesheet" /> ``` ## Usage Methods ### 1. Webfont (HTML/CSS) Import CSS and use class names: ```tsx // Next.js - app/layout.tsx import 'remixicon/fonts/remixicon.css' export default function RootLayout({ children }) { return ( <html> <body>{children}</body> </html> ) } // Use in components <i className="ri-home-line"></i> <i className="ri-search-fill"></i> ``` **With sizing classes:** ```html <i className="ri-home-line ri-2x"></i> <!-- 2em --> <i className="ri-search-line ri-lg"></i> <!-- 1.33em --> <i className="ri-heart-fill ri-xl"></i> <!-- 1.5em --> ``` **Available sizes:** - `ri-xxs` (0.5em) - `ri-xs` (0.75em) - `ri-sm` (0.875em) - `ri-1x` (1em) - `ri-lg` (1.33em) - `ri-xl` (1.5em) - `ri-2x` through `ri-10x` - `ri-fw` (fixed width) ### 2. React Components ```tsx import { RiHomeLine, RiSearchFill, RiHeartLine } from "@remixicon/react" export function MyComponent() { return ( <div> <RiHomeLine size={24} /> <RiSearchFill size={32} color="blue" /> <RiHeartLine size="1.5em" className="icon" /> </div> ) } ``` **Props:** - `size` - Number (pixels) or string (em, rem) - `color` - CSS color value - `className` - CSS class - Standard SVG props (onClick, style, etc.) ### 3. Vue 3 Components ```vue <script setup lang="ts"> import { RiHomeLine, RiSearchFill } from "@remixicon/vue" </script> <template> <div> <RiHomeLine :size="24" /> <RiSearchFill :size="32" color="blue" /> </div> </template> ``` ### 4. Direct SVG ```tsx // Download SVG file and import import HomeIcon from '@/icons/home-line.svg' export function Component() { return <img src={HomeIcon} alt="Home" width={24} height={24} /> } ``` ### 5. SVG Sprite ```html <svg className="icon"> <use xlinkHref="path/to/remixicon.symbol.svg#ri-home-line"></use> </svg> ``` ```css .icon { width: 24px; height: 24px; fill: currentColor; } ``` ## Icon Categories 20 semantic categories with 3,100+ icons: **Navigation & UI:** - Arrows (arrow-left, arrow-right, arrow-up-down) - System (settings, delete, add, close, more) - Editor (bold, italic, link, list, code) **Communication:** - Communication (chat, phone, mail, message) - User (user, account, team, contacts) **Media & Content:** - Media (play, pause, volume, camera, video) - Document (file, folder, article, draft) - Design (brush, palette, magic, crop) **Business & Commerce:** - Business (briefcase, pie-chart, bar-chart) - Finance (money, wallet, bank-card, coin) - Map (map, pin, compass, navigation) **Objects & Places:** - Buildings (home, bank, hospital, store) - Device (phone, laptop, tablet, printer) - Food (restaurant, cake, cup, knife) - Weather (sun, cloud, rain, moon) **Development & Logos:** - Development (code, terminal, bug, git-branch) - Logos (github, twitter, facebook, google) **Health & Medical:** - Health (heart-pulse, capsule, stethoscope) ## Common Patterns ### Navigation Menu ```tsx // Webfont approach export function Navigation() { return ( <nav> <a href="/home"> <i className="ri-home-line"></i> <span>Home</span> </a> <a href="/search"> <i className="ri-search-line"></i> <span>Search</span> </a> <a href="/profile"> <i className="ri-user-line"></i> <span>Profile</span> </a> </nav> ) } // React component approach import { RiHomeLine, RiSearchLine, RiUserLine } from "@remixicon/react" export function Navigation() { return ( <nav> <a href="/home"> <RiHomeLine size={20} /> <span>Home</span> </a> <a href="/search"> <RiSearchLine size={20} /> <span>Search</span> </a> <a href="/profile"> <RiUserLine size={20} /> <span>Profile</span> </a> </nav> ) } ``` ### Button with Icon ```tsx import { RiDownloadLine } from "@remixicon/react" export function DownloadButton() { return ( <button className="btn-primary"> <RiDownloadLine size={18} /> <span>Download</span> </button> ) } ``` ### Status Indicators ```tsx import { RiCheckboxCircleFill, RiErrorWarningFill, RiAlertFill, RiInformationFill } from "@remixicon/react" type Status = 'success' | 'error' | 'warning' | 'info' export function StatusIcon({ status }: { status: Status }) { const icons = { success: <RiCheckboxCircleFill color="green" size={20} />, error: <RiErrorWarningFill color="red" size={20} />, warning: <RiAlertFill color="orange" size={20} />, info: <RiInformationFill color="blue" size={20} /> } return icons[status] } ``` ### Input with Icon ```tsx import { RiSearchLine } from "@remixicon/react" export function SearchInput() { return ( <div className="input-group"> <RiSearchLine size={20} className="input-icon" /> <input type="text" placeholder="Search..." /> </div> ) } ``` ```css .input-group { position: relative; } .input-icon { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); color: #666; } input { padding-left: 40px; } ``` ### Dynamic Icon Selection ```tsx import { RiHomeLine, RiHeartFill, RiStarLine } from "@remixicon/react" const iconMap = { home: RiHomeLine, heart: RiHeartFill, star: RiStarLine, } export function DynamicIcon({ name, size = 24 }: { name: string; size?: number }) { const Icon = iconMap[name] return Icon ? <Icon size={size} /> : null } // Usage <DynamicIcon name="home" size={24} /> ``` ## Styling & Customization ### Color ```tsx // Inherit from parent <i className="ri-home-line" style={{ color: 'blue' }}></i> // React component <RiHomeLine color="blue" /> <RiHomeLine color="#ff0000" /> <RiHomeLine color="rgb(255, 0, 0)" /> ``` ### Size ```tsx // CSS class <i className="ri-home-line ri-2x"></i> // Inline style <i className="ri-home-line" style={{ fontSize: '32px' }}></i> // React component <RiHomeLine size={32} /> <RiHomeLine size="2em" /> ``` ### Responsive Sizing ```css .icon { font-size: 24px; } @media (max-width: 768px) { .icon { font-size: 20px; } } ``` ### Animations ```css .spin { animation: spin 1s linear infinite; } @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } ``` ```tsx <i className="ri-loader-4-line spin"></i> ``` ### Hover Effects ```css .icon-button { transition: color 0.2s; } .icon-button:hover { color: #007bff; } ``` ## Accessibility ### Provide Labels **Icon-only buttons:** ```tsx <button aria-label="Search"> <i className="ri-search-line"></i> </button> // Or with React <button aria-label="Search"> <RiSearchLine size={20} /> </button> ``` ### Decorative Icons Hide from screen readers: ```tsx <span aria-hidden="true"> <i className="ri-star-fill"></i> </span> // React <span aria-hidden="true"> <RiStarFill size={16} /> </span> ``` ### Icon with Text ```tsx <button> <RiDownloadLine size={18} aria-hidden="true" /> <span>Download</span> </button> ``` Text provides context, icon is decorative. ## Framework Integration ### Next.js ```tsx // app/layout.tsx import 'remixicon/fonts/remixicon.css' export default function RootLayout({ children }) { return ( <html> <body>{children}</body> </html> ) } // app/page.tsx import { RiHomeLine } from "@remixicon/react" export default function Page() { return <RiHomeLine size={24} /> } ``` ### Tailwind CSS ```tsx <i className="ri-home-line text-2xl text-blue-500"></i> <RiHomeLine size={24} className="text-blue-500 hover:text-blue-600" /> ``` ### CSS Modules ```tsx import styles from './component.module.css' import 'remixicon/fonts/remixicon.css' export function Component() { return <i className={`ri-home-line ${styles.icon}`}></i> } ``` ## Performance Considerations ### Webfont (Recommended for Multiple Icons) **Pros:** - Single HTTP request - All icons available - Easy to use **Cons:** - 179KB WOFF2 file - Loads all icons even if unused **Best for:** Apps using 10+ different icons ### Individual SVG (Recommended for Few Icons) **Pros:** - Only load what you need - Smallest bundle size - Tree-shakeable with React package **Cons:** - Multiple imports **Best for:** Apps using 1-5 icons ### React/Vue Package **Pros:** - Tree-shakeable (only imports used icons) - TypeScript support - Component API **Cons:** - Slightly larger than raw SVG - Requires React/Vue **Best for:** React/Vue apps with TypeScript ## Troubleshooting ### Icons Not Displaying **Check CSS import:** ```tsx import 'remixicon/fonts/remixicon.css' ``` **Verify class name:** ```html <!-- Correct --> <i className="ri-home-line"></i> <!-- Incorrect --> <i className="ri-home"></i> <i className="home-line"></i> ``` **Check font loading:** ```css /* Ensure font-family is applied */ [class^="ri-"], [class*=" ri-"] { font-family: "remixicon" !important; } ``` ### Icons Look Blurry Use multiples of 24px for crisp rendering: ```tsx // Good <RiHomeLine size={24} /> <RiHomeLine size={48} /> // Bad (breaks pixel grid) <RiHomeLine size={20} /> <RiHomeLine size={30} /> ``` ### Wrong Icon Size **Set parent font-size:** ```css .icon-container { font-size: 24px; } ``` **Or use size prop:** ```tsx <RiHomeLine size={24} /> ``` ## Best Practices 1. **Choose style consistently** - Use line or fill throughout app 2. **Maintain 24px grid** - Use sizes: 24, 48, 72, 96px 3. **Provide accessibility** - Add aria-labels to icon-only buttons 4. **Use currentColor** - Icons inherit text color by default 5. **Optimize bundle** - Use React package for tree-shaking 6. **Cache webfonts** - CDN or long cache headers 7. **Lazy load icons** - Dynamic import for heavy icon sets 8. **Test on devices** - Ensure icons scale properly 9. **Document usage** - Create icon component library 10. **Version lock** - Pin RemixIcon version for consistency ## Resources - Website: https://remixicon.com - GitHub: https://github.com/Remix-Design/RemixIcon - React Package: @remixicon/react - Vue Package: @remixicon/vue - License: Apache 2.0 - Total Icons: 3,100+ - Current Version: 4.7.0 -
turborepo-caching.md 9.5 KB
# Turborepo Caching Strategies Local caching, remote caching, cache invalidation, and optimization techniques. ## Local Caching ### How It Works Turborepo caches task outputs based on inputs: 1. **Hash inputs**: Source files, dependencies, environment variables, config 2. **Run task**: If hash not in cache 3. **Save outputs**: Store in `.turbo/cache` 4. **Restore on match**: Instant completion on cache hit Default cache location: `./node_modules/.cache/turbo` ### Cache Configuration ```json // turbo.json { "pipeline": { "build": { "outputs": ["dist/**", ".next/**", "!.next/cache/**"], "cache": true // default }, "dev": { "cache": false // don't cache dev servers } } } ``` ### Outputs Configuration Specify what gets cached: ```json { "build": { "outputs": [ "dist/**", // All files in dist "build/**", // Build directory ".next/**", // Next.js output "!.next/cache/**", // Exclude Next.js cache "storybook-static/**", // Storybook build "*.tsbuildinfo" // TypeScript build info ] } } ``` **Best practices:** - Include all build artifacts - Exclude nested caches - Include type definitions - Include generated files ### Clear Local Cache ```bash # Remove cache directory rm -rf ./node_modules/.cache/turbo # Or use turbo command with --force turbo run build --force # Clear and rebuild turbo run clean && turbo run build ``` ## Remote Caching Share cache across team and CI/CD. ### Vercel Remote Cache (Recommended) **Setup:** ```bash # Login to Vercel turbo login # Link repository turbo link ``` **Use in CI:** ```yaml # .github/workflows/ci.yml env: TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: ${{ secrets.TURBO_TEAM }} steps: - run: turbo run build test ``` Get tokens from Vercel dashboard: 1. Go to https://vercel.com/account/tokens 2. Create new token 3. Add as GitHub secrets ### Custom Remote Cache Configure custom remote cache server: ```json // .turbo/config.json { "teamid": "team_xxx", "apiurl": "https://cache.example.com", "token": "your-token" } ``` Or use environment variables: ```bash export TURBO_API="https://cache.example.com" export TURBO_TOKEN="your-token" export TURBO_TEAM="team_xxx" ``` ### Remote Cache Verification ```bash # Check cache status turbo run build --output-logs=hash-only # Output shows: # • web:build: cache hit, replaying logs [hash] # • api:build: cache miss, executing [hash] ``` ## Cache Signatures Cache invalidated when these change: ### 1. Source Files All tracked Git files in package: ``` packages/ui/ ├── src/ │ ├── button.tsx # Tracked │ └── input.tsx # Tracked ├── dist/ # Ignored (in .gitignore) └── node_modules/ # Ignored ``` ### 2. Package Dependencies Changes in package.json: ```json { "dependencies": { "react": "18.2.0" // Version change invalidates cache } } ``` ### 3. Environment Variables Configured in pipeline: ```json { "build": { "env": ["NODE_ENV", "API_URL"] // Changes invalidate cache } } ``` ### 4. Global Dependencies Files affecting all packages: ```json { "globalDependencies": [ "**/.env.*local", "tsconfig.json", ".eslintrc.js" ] } ``` ### 5. Task Configuration Changes to turbo.json pipeline: ```json { "build": { "dependsOn": ["^build"], "outputs": ["dist/**"] // Config changes invalidate cache } } ``` ## Input Control ### Override Input Detection Explicitly define what affects cache: ```json { "build": { "inputs": [ "src/**/*.ts", // Include TS files "src/**/*.tsx", // Include TSX files "!src/**/*.test.ts", // Exclude tests "!src/**/*.stories.tsx", // Exclude stories "package.json", // Include package.json "tsconfig.json" // Include config ] } } ``` Use cases: - Exclude test files from build cache - Exclude documentation from production builds - Include only source files, not generated files ### Global vs Package Inputs **Global inputs** (affect all packages): ```json { "globalDependencies": [".env", "tsconfig.json"] } ``` **Package inputs** (affect specific tasks): ```json { "pipeline": { "build": { "inputs": ["src/**"] } } } ``` ## Environment Variables ### Cached Environment Variables Include in cache signature: ```json { "pipeline": { "build": { "env": [ "NODE_ENV", // Must match for cache hit "NEXT_PUBLIC_API_URL", "DATABASE_URL" ] } } } ``` Cache invalidated when values change. ### Pass-Through Environment Variables Don't affect cache: ```json { "pipeline": { "build": { "passThroughEnv": [ "DEBUG", // Different values use same cache "LOG_LEVEL", "VERBOSE" ] } } } ``` Use for: Debug flags, log levels, non-production settings ### Global Environment Variables Available to all tasks: ```json { "globalEnv": [ "NODE_ENV", "CI", "VERCEL" ] } ``` ## Cache Optimization Strategies ### 1. Granular Outputs Define precise outputs to minimize cache size: ```json // ❌ Bad - caches too much { "build": { "outputs": ["**"] } } // ✅ Good - specific outputs { "build": { "outputs": ["dist/**", "!dist/**/*.map"] } } ``` ### 2. Exclude Unnecessary Files ```json { "build": { "outputs": [ ".next/**", "!.next/cache/**", // Exclude Next.js cache "!.next/server/**/*.js.map", // Exclude source maps "!.next/static/**/*.map" ] } } ``` ### 3. Separate Cacheable Tasks ```json { "pipeline": { "build": { "dependsOn": ["^build"], "cache": true }, "test": { "dependsOn": ["build"], "cache": true // Separate from build }, "dev": { "cache": false // Never cache } } } ``` ### 4. Use Input Filters Only track relevant files: ```json { "build": { "inputs": [ "src/**/*.{ts,tsx}", "!src/**/*.{test,spec}.{ts,tsx}", "public/**", "package.json" ] } } ``` ## Cache Analysis ### Inspect Cache Hits/Misses ```bash # Dry run with JSON output turbo run build --dry-run=json | jq '.tasks[] | {package: .package, task: .task, cache: .cache}' ``` ### View Task Graph ```bash # Generate task graph turbo run build --graph # Output: graph.html (open in browser) ``` ### Cache Statistics ```bash # Run with summary turbo run build --summarize # Output: .turbo/runs/[hash].json ``` ## CI/CD Cache Configuration ### GitHub Actions ```yaml 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: 18 - name: Install dependencies run: npm install - name: Build and test run: turbo run build test lint env: TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: ${{ secrets.TURBO_TEAM }} # Optional: Cache node_modules - uses: actions/cache@v3 with: path: node_modules key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} ``` ### GitLab CI ```yaml image: node:18 cache: key: ${CI_COMMIT_REF_SLUG} paths: - node_modules/ - .turbo/ build: stage: build script: - npm install - turbo run build test variables: TURBO_TOKEN: $TURBO_TOKEN TURBO_TEAM: $TURBO_TEAM ``` ## Troubleshooting ### Cache Not Working **Check outputs are defined:** ```bash turbo run build --dry-run=json | jq '.tasks[] | {task: .task, outputs: .outputs}' ``` **Verify cache location:** ```bash ls -la ./node_modules/.cache/turbo ``` **Check environment variables:** ```bash echo $TURBO_TOKEN echo $TURBO_TEAM ``` ### Cache Too Large **Analyze cache size:** ```bash du -sh ./node_modules/.cache/turbo ``` **Reduce outputs:** ```json { "build": { "outputs": [ "dist/**", "!dist/**/*.map", // Exclude source maps "!dist/**/*.test.js" // Exclude test files ] } } ``` **Clear old cache:** ```bash # Turborepo doesn't auto-clean, manually remove: rm -rf ./node_modules/.cache/turbo ``` ### Remote Cache Connection Issues **Test connection:** ```bash curl -I https://cache.example.com ``` **Verify token:** ```bash turbo link # Should show: "Remote caching enabled" ``` **Check logs:** ```bash turbo run build --output-logs=full ``` ## Best Practices 1. **Define precise outputs** - Only cache necessary files 2. **Exclude nested caches** - Don't cache caches (.next/cache) 3. **Use remote caching** - Share cache across team and CI 4. **Track relevant inputs** - Use `inputs` to filter files 5. **Separate env vars** - Use `passThroughEnv` for debug flags 6. **Cache test results** - Include coverage in outputs 7. **Don't cache dev servers** - Set `cache: false` for dev tasks 8. **Use global dependencies** - Share config across packages 9. **Monitor cache performance** - Use `--summarize` to analyze 10. **Clear cache periodically** - Remove stale cache manually ## Cache Performance Tips **For CI/CD:** - Enable remote caching - Run only changed packages: `--filter='...[origin/main]'` - Use `--continue` to see all errors - Cache node_modules separately **For Local Development:** - Keep local cache enabled - Don't force rebuild unless needed - Use filters to build only what changed - Clear cache if issues arise **For Large Monorepos:** - Use granular outputs - Implement input filters - Monitor cache size regularly - Consider cache size limits on remote cache -
turborepo-pipelines.md 8.3 KB
# Turborepo Task Pipelines Task orchestration, dependencies, and parallel execution strategies. ## Pipeline Configuration Define tasks in `turbo.json`: ```json { "$schema": "https://turbo.build/schema.json", "pipeline": { "build": { "dependsOn": ["^build"], "outputs": ["dist/**", ".next/**"] }, "test": { "dependsOn": ["build"], "outputs": ["coverage/**"] }, "lint": {}, "dev": { "cache": false, "persistent": true } } } ``` ## Task Dependencies ### Topological Dependencies (^) `^` means "run this task in dependencies first": ```json { "pipeline": { "build": { "dependsOn": ["^build"] } } } ``` Example flow: ``` packages/ui (dependency) ↓ builds first apps/web (depends on @repo/ui) ↓ builds second ``` ### Internal Dependencies Run tasks in same package first: ```json { "pipeline": { "deploy": { "dependsOn": ["build", "test"] } } } ``` Execution order in same package: 1. Run `build` 2. Run `test` 3. Run `deploy` ### Combined Dependencies Mix topological and internal: ```json { "pipeline": { "test": { "dependsOn": ["^build", "lint"] } } } ``` Execution order: 1. Build all dependencies (`^build`) 2. Lint current package (`lint`) 3. Run tests (`test`) ## Task Configuration Options ### outputs Define what gets cached: ```json { "build": { "outputs": [ "dist/**", // All files in dist ".next/**", // Next.js build "!.next/cache/**", // Exclude Next.js cache "build/**", // Build directory "public/dist/**" // Public assets ] } } ``` ### cache Enable/disable caching: ```json { "dev": { "cache": false // Don't cache dev server }, "build": { "cache": true // Cache build (default) } } ``` ### persistent Keep task running (for dev servers): ```json { "dev": { "cache": false, "persistent": true // Don't kill after completion } } ``` ### env Environment variables affecting output: ```json { "build": { "env": [ "NODE_ENV", "NEXT_PUBLIC_API_URL", "DATABASE_URL" ] } } ``` ### passThroughEnv Pass env vars without affecting cache: ```json { "build": { "passThroughEnv": [ "DEBUG", // Pass through but don't invalidate cache "LOG_LEVEL" ] } } ``` ### inputs Override default input detection: ```json { "build": { "inputs": [ "src/**/*.ts", "!src/**/*.test.ts", // Exclude test files "package.json" ] } } ``` ### outputMode Control output display: ```json { "build": { "outputMode": "full" // Show all output }, "dev": { "outputMode": "hash-only" // Show cache hash only }, "test": { "outputMode": "new-only" // Show new output only }, "lint": { "outputMode": "errors-only" // Show errors only } } ``` ## Running Tasks ### Basic Execution ```bash # Run build in all packages turbo run build # Run multiple tasks turbo run build test lint # Run with specific package manager pnpm turbo run build ``` ### Filtering Run tasks in specific packages: ```bash # Single package turbo run build --filter=web turbo run build --filter=@repo/ui # Multiple packages turbo run build --filter=web --filter=api # All apps turbo run build --filter='./apps/*' # Pattern matching turbo run test --filter='*-api' ``` ### Dependency Filtering ```bash # Package and its dependencies turbo run build --filter='...web' # Package's dependencies only (exclude package itself) turbo run build --filter='...^web' # Package and its dependents turbo run test --filter='ui...' # Package's dependents only turbo run test --filter='^ui...' ``` ### Git-Based Filtering Run only on changed packages: ```bash # Changed since main branch turbo run build --filter='[main]' # Changed since HEAD~1 turbo run build --filter='[HEAD~1]' # Changed in working directory turbo run test --filter='...[HEAD]' # Package and dependencies, only if changed turbo run build --filter='...[origin/main]' ``` ## Concurrency Control ### Parallel Execution (Default) Turborepo runs tasks in parallel when safe: ```bash # Run with default parallelism turbo run build ``` ### Limit Concurrency ```bash # Max 3 tasks at once turbo run build --concurrency=3 # 50% of CPU cores turbo run build --concurrency=50% # No parallelism (sequential) turbo run build --concurrency=1 ``` ### Continue on Error ```bash # Don't stop on first error turbo run test --continue ``` ## Task Execution Order Example monorepo: ``` apps/ ├── web (depends on @repo/ui, @repo/utils) └── docs (depends on @repo/ui) packages/ ├── ui (depends on @repo/utils) └── utils (no dependencies) ``` With config: ```json { "pipeline": { "build": { "dependsOn": ["^build"] } } } ``` Execution order for `turbo run build`: 1. **Wave 1** (parallel): `@repo/utils` (no dependencies) 2. **Wave 2** (parallel): `@repo/ui` (depends on utils) 3. **Wave 3** (parallel): `web` and `docs` (both depend on ui) ## Complex Pipeline Examples ### Full-Stack Application ```json { "pipeline": { "build": { "dependsOn": ["^build"], "outputs": [".next/**", "dist/**"] }, "test": { "dependsOn": ["^build"], "outputs": ["coverage/**"] }, "lint": { "dependsOn": ["^build"] }, "typecheck": { "dependsOn": ["^build"] }, "dev": { "cache": false, "persistent": true }, "deploy": { "dependsOn": ["build", "test", "lint", "typecheck"] } } } ``` ### Monorepo with Code Generation ```json { "pipeline": { "generate": { "cache": false, "outputs": ["src/generated/**"] }, "build": { "dependsOn": ["^build", "generate"], "outputs": ["dist/**"] }, "test": { "dependsOn": ["generate"], "outputs": ["coverage/**"] } } } ``` ### Database-Dependent Pipeline ```json { "pipeline": { "db:generate": { "cache": false }, "db:migrate": { "cache": false }, "build": { "dependsOn": ["^build", "db:generate"], "outputs": ["dist/**"] }, "test:unit": { "dependsOn": ["build"] }, "test:integration": { "dependsOn": ["db:migrate"], "cache": false } } } ``` ## Dry Run Preview execution without running: ```bash # See what would run turbo run build --dry-run # JSON output for scripts turbo run build --dry-run=json # Show full task graph turbo run build --graph ``` ## Force Execution Ignore cache and run tasks: ```bash # Force rebuild everything turbo run build --force # Force specific package turbo run build --filter=web --force ``` ## Output Control ```bash # Show only errors turbo run build --output-logs=errors-only # Show new logs only turbo run build --output-logs=new-only # Show cache hash only turbo run build --output-logs=hash-only # Show full output turbo run build --output-logs=full ``` ## Best Practices 1. **Use topological dependencies** - `^build` ensures correct build order 2. **Cache build outputs** - Define `outputs` for faster rebuilds 3. **Disable cache for dev** - Set `cache: false` for dev servers 4. **Mark persistent tasks** - Use `persistent: true` for long-running tasks 5. **Filter strategically** - Use filters to run only affected tasks 6. **Control concurrency** - Limit parallelism for resource-intensive tasks 7. **Configure env vars** - Include vars that affect output in `env` 8. **Use dry-run** - Preview execution plan before running 9. **Continue on error in CI** - Use `--continue` to see all errors 10. **Leverage git filtering** - Run only on changed packages in CI ## Common Patterns ### CI/CD Pipeline ```yaml # .github/workflows/ci.yml jobs: build: steps: - run: turbo run build test lint --filter='...[origin/main]' ``` Only build/test/lint changed packages and their dependents. ### Development Workflow ```bash # Start all dev servers turbo run dev # Start specific app with dependencies turbo run dev --filter=web... ``` ### Pre-commit Hook ```json // package.json { "scripts": { "pre-commit": "turbo run lint test --filter='...[HEAD]'" } } ``` Only lint/test changed packages. ### Deployment ```bash # Build and test specific app turbo run build test --filter=web... # Deploy if successful turbo run deploy --filter=web ``` Build app and its dependencies, then deploy. -
turborepo-setup.md 9.5 KB
# Turborepo Setup & Configuration Installation, workspace configuration, and project structure for monorepos. ## Installation ### Create New Monorepo Using official starter: ```bash npx create-turbo@latest my-monorepo cd my-monorepo ``` Interactive prompts: - Project name - Package manager (npm, yarn, pnpm, bun) - Example template ### Manual Installation Install in existing project: ```bash # npm npm install turbo --save-dev # yarn yarn add turbo --dev # pnpm pnpm add turbo --save-dev # bun bun add turbo --dev ``` ## Workspace Configuration ### Package Manager Setup **pnpm (recommended):** ```yaml # pnpm-workspace.yaml packages: - 'apps/*' - 'packages/*' ``` **npm/yarn:** ```json // package.json (root) { "name": "my-monorepo", "private": true, "workspaces": [ "apps/*", "packages/*" ] } ``` ### Root Package.json ```json { "name": "my-monorepo", "private": true, "workspaces": ["apps/*", "packages/*"], "scripts": { "build": "turbo run build", "dev": "turbo run dev", "lint": "turbo run lint", "test": "turbo run test", "clean": "turbo run clean" }, "devDependencies": { "turbo": "latest", "typescript": "^5.0.0" }, "packageManager": "pnpm@8.0.0" } ``` ## Project Structure ### Recommended Directory Structure ``` my-monorepo/ ├── apps/ # Applications │ ├── web/ # Next.js web app │ │ ├── app/ │ │ ├── package.json │ │ └── next.config.js │ ├── docs/ # Documentation site │ │ ├── app/ │ │ └── package.json │ └── api/ # Backend API │ ├── src/ │ └── package.json ├── packages/ # Shared packages │ ├── ui/ # UI component library │ │ ├── src/ │ │ ├── package.json │ │ └── tsconfig.json │ ├── config/ # Shared configs │ │ ├── eslint/ │ │ └── typescript/ │ ├── utils/ # Utility functions │ │ ├── src/ │ │ └── package.json │ └── types/ # Shared TypeScript types │ ├── src/ │ └── package.json ├── turbo.json # Turborepo config ├── package.json # Root package.json ├── pnpm-workspace.yaml # Workspace config (pnpm) └── .gitignore ``` ## Application Package Setup ### Next.js App ```json // apps/web/package.json { "name": "web", "version": "1.0.0", "private": true, "scripts": { "dev": "next dev", "build": "next build", "start": "next start", "lint": "next lint" }, "dependencies": { "@repo/ui": "*", "@repo/utils": "*", "next": "latest", "react": "latest", "react-dom": "latest" }, "devDependencies": { "@repo/typescript-config": "*", "@repo/eslint-config": "*", "typescript": "^5.0.0" } } ``` ### Backend API App ```json // apps/api/package.json { "name": "api", "version": "1.0.0", "private": true, "scripts": { "dev": "tsx watch src/index.ts", "build": "tsup src/index.ts", "start": "node dist/index.js", "lint": "eslint src/" }, "dependencies": { "@repo/utils": "*", "@repo/types": "*", "express": "^4.18.0" }, "devDependencies": { "@repo/typescript-config": "*", "@types/express": "^4.17.0", "tsx": "^4.0.0", "tsup": "^8.0.0" } } ``` ## Shared Package Setup ### UI Component Library ```json // packages/ui/package.json { "name": "@repo/ui", "version": "0.0.0", "main": "./dist/index.js", "types": "./dist/index.d.ts", "exports": { ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" }, "./button": { "types": "./dist/button.d.ts", "default": "./dist/button.js" } }, "scripts": { "build": "tsc", "dev": "tsc --watch", "lint": "eslint src/", "clean": "rm -rf dist" }, "dependencies": { "react": "latest" }, "devDependencies": { "@repo/typescript-config": "*", "typescript": "^5.0.0" } } ``` ```json // packages/ui/tsconfig.json { "extends": "@repo/typescript-config/react-library.json", "compilerOptions": { "outDir": "dist", "declarationDir": "dist" }, "include": ["src"], "exclude": ["node_modules", "dist"] } ``` ### Utility Library ```json // packages/utils/package.json { "name": "@repo/utils", "version": "0.0.0", "main": "./dist/index.js", "types": "./dist/index.d.ts", "scripts": { "build": "tsc", "dev": "tsc --watch", "test": "jest" }, "devDependencies": { "@repo/typescript-config": "*", "jest": "^29.0.0", "typescript": "^5.0.0" } } ``` ## Shared Configuration Packages ### TypeScript Config Package ``` packages/typescript-config/ ├── base.json ├── nextjs.json ├── react-library.json └── package.json ``` ```json // packages/typescript-config/package.json { "name": "@repo/typescript-config", "version": "0.0.0", "main": "base.json", "files": [ "base.json", "nextjs.json", "react-library.json" ] } ``` ```json // packages/typescript-config/base.json { "compilerOptions": { "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "isolatedModules": true, "moduleResolution": "bundler", "target": "ES2020", "module": "ESNext" }, "exclude": ["node_modules"] } ``` ```json // packages/typescript-config/nextjs.json { "extends": "./base.json", "compilerOptions": { "jsx": "preserve", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "noEmit": true, "incremental": true, "plugins": [{ "name": "next" }] }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "exclude": ["node_modules"] } ``` ### ESLint Config Package ``` packages/eslint-config/ ├── library.js ├── next.js └── package.json ``` ```json // packages/eslint-config/package.json { "name": "@repo/eslint-config", "version": "0.0.0", "main": "library.js", "files": ["library.js", "next.js"], "dependencies": { "eslint-config-next": "latest", "eslint-config-prettier": "^9.0.0", "eslint-plugin-react": "latest" } } ``` ```js // packages/eslint-config/library.js module.exports = { extends: ['eslint:recommended', 'prettier'], env: { node: true, es2020: true, }, parserOptions: { ecmaVersion: 2020, sourceType: 'module', }, rules: { 'no-console': 'warn', }, } ``` ```js // packages/eslint-config/next.js module.exports = { extends: ['next', 'prettier'], rules: { '@next/next/no-html-link-for-pages': 'off', }, } ``` ## Dependency Management ### Internal Dependencies Use workspace protocol: **pnpm:** ```json { "dependencies": { "@repo/ui": "workspace:*" } } ``` **npm/yarn:** ```json { "dependencies": { "@repo/ui": "*" } } ``` ### Version Syncing Keep dependencies in sync across packages: ```json // Root package.json { "devDependencies": { "react": "18.2.0", "react-dom": "18.2.0", "typescript": "5.0.0" } } ``` Packages inherit from root or specify versions explicitly. ## Turbo.json Configuration Basic configuration file: ```json { "$schema": "https://turbo.build/schema.json", "globalDependencies": [ "**/.env.*local", "tsconfig.json" ], "globalEnv": [ "NODE_ENV" ], "pipeline": { "build": { "dependsOn": ["^build"], "outputs": [".next/**", "!.next/cache/**", "dist/**"] }, "dev": { "cache": false, "persistent": true }, "lint": { "dependsOn": ["^build"] }, "test": { "dependsOn": ["build"], "outputs": ["coverage/**"] }, "clean": { "cache": false } } } ``` ## Environment Variables ### Global Environment Variables ```json // turbo.json { "globalEnv": [ "NODE_ENV", "CI" ] } ``` ### Package-Specific Environment Variables ```json { "pipeline": { "build": { "env": ["NEXT_PUBLIC_API_URL", "DATABASE_URL"], "passThroughEnv": ["CUSTOM_VAR"] } } } ``` ### .env Files ``` my-monorepo/ ├── .env # Global env vars ├── .env.local # Local overrides (gitignored) ├── apps/ │ └── web/ │ ├── .env # App-specific │ └── .env.local # Local overrides ``` ## Gitignore Configuration ```gitignore # Dependencies node_modules/ .pnp .pnp.js # Turbo .turbo # Build outputs dist/ .next/ out/ build/ # Environment .env.local .env.*.local # Testing coverage/ # Misc .DS_Store *.log ``` ## NPM Scripts Common scripts in root package.json: ```json { "scripts": { "build": "turbo run build", "dev": "turbo run dev", "lint": "turbo run lint", "test": "turbo run test", "format": "prettier --write \"**/*.{ts,tsx,md}\"", "clean": "turbo run clean && rm -rf node_modules", "typecheck": "turbo run typecheck" } } ``` ## Initialization Checklist Setting up new Turborepo: - [ ] Install Turborepo (create-turbo or manual) - [ ] Configure workspace (pnpm-workspace.yaml or package.json) - [ ] Create directory structure (apps/, packages/) - [ ] Set up shared config packages (typescript-config, eslint-config) - [ ] Create turbo.json with pipeline - [ ] Configure gitignore - [ ] Set up environment variables - [ ] Define package dependencies - [ ] Add root scripts - [ ] Test build and dev commands
-
-
scripts
-
tests
-
coverage-web.json 34 KB
{"meta": {"format": 3, "version": "7.11.0", "timestamp": "2025-11-05T00:56:58.689936", "branch_coverage": false, "show_contexts": false}, "files": {"__init__.py": {"executed_lines": [0], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": [], "functions": {"": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "nextjs_init.py": {"executed_lines": [2, 8, 9, 10, 11, 12, 13, 14, 17, 18, 20, 44, 45, 46, 47, 48, 49, 50, 51, 53, 55, 56, 58, 59, 63, 64, 66, 68, 69, 71, 73, 76, 79, 81, 82, 83, 84, 85, 87, 88, 89, 92, 93, 94, 96, 98, 101, 102, 105, 106, 109, 110, 112, 113, 115, 117, 120, 121, 124, 125, 127, 129, 132, 133, 138, 139, 142, 143, 144, 149, 150, 151, 156, 157, 158, 159, 161, 162, 165, 166, 169, 170, 172, 174, 180, 182, 183, 192, 193, 194, 196, 197, 198, 199, 201, 215, 217, 221, 222, 240, 255, 257, 267, 269, 282, 284, 306, 308, 310, 312, 313, 328, 342, 344, 352, 354, 359, 361, 379, 381, 416, 418, 460, 462, 467, 469, 479, 481, 482, 483, 484, 485, 486, 487, 489, 490, 491, 492, 494, 495, 496, 497, 498, 499, 500, 501, 508, 546], "summary": {"covered_lines": 146, "num_statements": 162, "percent_covered": 90.12345679012346, "percent_covered_display": "90", "missing_lines": 16, "excluded_lines": 0}, "missing_lines": [503, 504, 505, 510, 513, 514, 517, 520, 523, 524, 525, 526, 530, 532, 543, 547], "excluded_lines": [], "functions": {"NextJSInitializer.__init__": {"executed_lines": [44, 45, 46, 47, 48, 49, 50, 51], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "NextJSInitializer.validate_name": {"executed_lines": [55, 56, 58, 59, 63, 64], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "NextJSInitializer.check_directory": {"executed_lines": [68, 69], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "NextJSInitializer.create_directory_structure": {"executed_lines": [73, 76, 79, 81, 82, 83, 84, 85, 87, 88, 89, 92, 93, 94], "summary": {"covered_lines": 14, "num_statements": 14, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "NextJSInitializer._create_app_router_files": {"executed_lines": [98, 101, 102, 105, 106, 109, 110, 112, 113], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "NextJSInitializer._create_pages_router_files": {"executed_lines": [117, 120, 121, 124, 125], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "NextJSInitializer.create_config_files": {"executed_lines": [129, 132, 133, 138, 139, 142, 143, 144, 149, 150, 151, 156, 157, 158, 159, 161, 162, 165, 166, 169, 170], "summary": {"covered_lines": 21, "num_statements": 21, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "NextJSInitializer._get_package_json": {"executed_lines": [174, 180, 182, 183, 192, 193, 194, 196, 197, 198, 199, 201], "summary": {"covered_lines": 12, "num_statements": 12, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "NextJSInitializer._get_layout_content": {"executed_lines": [217, 221, 222, 240], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "NextJSInitializer._get_page_content": {"executed_lines": [257], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "NextJSInitializer._get_next_config": {"executed_lines": [269], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "NextJSInitializer._get_tsconfig": {"executed_lines": [284], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "NextJSInitializer._get_eslint_config": {"executed_lines": [308], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "NextJSInitializer._get_tailwind_config": {"executed_lines": [312, 313, 328], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "NextJSInitializer._get_postcss_config": {"executed_lines": [344], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "NextJSInitializer._get_tailwind_globals": {"executed_lines": [354], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "NextJSInitializer._get_basic_globals": {"executed_lines": [361], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "NextJSInitializer._get_gitignore": {"executed_lines": [381], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "NextJSInitializer._get_readme": {"executed_lines": [418], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "NextJSInitializer._get_app_content": {"executed_lines": [462], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "NextJSInitializer._get_index_content": {"executed_lines": [469], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "NextJSInitializer.initialize": {"executed_lines": [481, 482, 483, 484, 485, 486, 487, 489, 490, 491, 492, 494, 495, 496, 497, 498, 499, 500, 501], "summary": {"covered_lines": 19, "num_statements": 22, "percent_covered": 86.36363636363636, "percent_covered_display": "86", "missing_lines": 3, "excluded_lines": 0}, "missing_lines": [503, 504, 505], "excluded_lines": []}, "main": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 12, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 12, "excluded_lines": 0}, "missing_lines": [510, 513, 514, 517, 520, 523, 524, 525, 526, 530, 532, 543], "excluded_lines": []}, "": {"executed_lines": [2, 8, 9, 10, 11, 12, 13, 14, 17, 18, 20, 53, 66, 71, 96, 115, 127, 172, 215, 255, 267, 282, 306, 310, 342, 352, 359, 379, 416, 460, 467, 479, 508, 546], "summary": {"covered_lines": 32, "num_statements": 33, "percent_covered": 96.96969696969697, "percent_covered_display": "97", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [547], "excluded_lines": []}}, "classes": {"NextJSInitializer": {"executed_lines": [44, 45, 46, 47, 48, 49, 50, 51, 55, 56, 58, 59, 63, 64, 68, 69, 73, 76, 79, 81, 82, 83, 84, 85, 87, 88, 89, 92, 93, 94, 98, 101, 102, 105, 106, 109, 110, 112, 113, 117, 120, 121, 124, 125, 129, 132, 133, 138, 139, 142, 143, 144, 149, 150, 151, 156, 157, 158, 159, 161, 162, 165, 166, 169, 170, 174, 180, 182, 183, 192, 193, 194, 196, 197, 198, 199, 201, 217, 221, 222, 240, 257, 269, 284, 308, 312, 313, 328, 344, 354, 361, 381, 418, 462, 469, 481, 482, 483, 484, 485, 486, 487, 489, 490, 491, 492, 494, 495, 496, 497, 498, 499, 500, 501], "summary": {"covered_lines": 114, "num_statements": 117, "percent_covered": 97.43589743589743, "percent_covered_display": "97", "missing_lines": 3, "excluded_lines": 0}, "missing_lines": [503, 504, 505], "excluded_lines": []}, "": {"executed_lines": [2, 8, 9, 10, 11, 12, 13, 14, 17, 18, 20, 53, 66, 71, 96, 115, 127, 172, 215, 255, 267, 282, 306, 310, 342, 352, 359, 379, 416, 460, 467, 479, 508, 546], "summary": {"covered_lines": 32, "num_statements": 45, "percent_covered": 71.11111111111111, "percent_covered_display": "71", "missing_lines": 13, "excluded_lines": 0}, "missing_lines": [510, 513, 514, 517, 520, 523, 524, 525, 526, 530, 532, 543, 547], "excluded_lines": []}}}, "tests/test_nextjs_init.py": {"executed_lines": [1, 3, 4, 5, 7, 10, 12, 15, 16, 18, 20, 25, 26, 27, 28, 29, 30, 32, 34, 36, 37, 41, 43, 45, 52, 53, 58, 59, 61, 63, 64, 66, 71, 72, 74, 76, 77, 83, 86, 87, 88, 89, 92, 93, 94, 96, 98, 99, 105, 108, 109, 110, 112, 114, 122, 124, 125, 126, 129, 130, 131, 132, 135, 136, 137, 140, 141, 142, 145, 148, 150, 152, 159, 161, 162, 163, 164, 165, 167, 169, 175, 177, 178, 179, 180, 182, 184, 190, 192, 193, 194, 196, 198, 205, 207, 208, 209, 211, 213, 220, 222, 223, 225, 227, 232, 234, 235, 236, 237, 239, 241, 246, 248, 249, 250, 252, 254, 255, 263, 264, 267, 268, 269, 270, 271, 272, 273, 274, 277, 278, 279, 281, 283, 284, 292, 295, 298, 299, 302, 303, 304, 306, 308, 309, 315, 318, 319], "summary": {"covered_lines": 145, "num_statements": 145, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": [], "functions": {"TestNextJSInitializer.test_init_with_defaults": {"executed_lines": [20, 25, 26, 27, 28, 29, 30], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestNextJSInitializer.test_validate_name_valid": {"executed_lines": [34, 36, 37, 41], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestNextJSInitializer.test_validate_name_invalid": {"executed_lines": [45, 52, 53, 58, 59], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestNextJSInitializer.test_check_directory_exists": {"executed_lines": [63, 64, 66, 71, 72], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestNextJSInitializer.test_create_directory_structure_app_router": {"executed_lines": [76, 77, 83, 86, 87, 88, 89, 92, 93, 94], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestNextJSInitializer.test_create_directory_structure_with_src": {"executed_lines": [98, 99, 105, 108, 109, 110], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestNextJSInitializer.test_package_json_generation": {"executed_lines": [114, 122, 124, 125, 126, 129, 130, 131, 132, 135, 136, 137, 140, 141, 142, 145, 148], "summary": {"covered_lines": 17, "num_statements": 17, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestNextJSInitializer.test_tsconfig_generation": {"executed_lines": [152, 159, 161, 162, 163, 164, 165], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestNextJSInitializer.test_layout_content_typescript": {"executed_lines": [169, 175, 177, 178, 179, 180], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestNextJSInitializer.test_layout_content_javascript": {"executed_lines": [184, 190, 192, 193, 194], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestNextJSInitializer.test_tailwind_config_typescript": {"executed_lines": [198, 205, 207, 208, 209], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestNextJSInitializer.test_tailwind_config_javascript": {"executed_lines": [213, 220, 222, 223], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestNextJSInitializer.test_gitignore_generation": {"executed_lines": [227, 232, 234, 235, 236, 237], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestNextJSInitializer.test_readme_generation": {"executed_lines": [241, 246, 248, 249, 250], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestNextJSInitializer.test_create_config_files": {"executed_lines": [254, 255, 263, 264, 267, 268, 269, 270, 271, 272, 273, 274, 277, 278, 279], "summary": {"covered_lines": 15, "num_statements": 15, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestNextJSInitializer.test_full_initialization": {"executed_lines": [283, 284, 292, 295, 298, 299, 302, 303, 304], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestNextJSInitializer.test_pages_router_structure": {"executed_lines": [308, 309, 315, 318, 319], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [1, 3, 4, 5, 7, 10, 12, 15, 16, 18, 32, 43, 61, 74, 96, 112, 150, 167, 182, 196, 211, 225, 239, 252, 281, 306], "summary": {"covered_lines": 24, "num_statements": 24, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"TestNextJSInitializer": {"executed_lines": [20, 25, 26, 27, 28, 29, 30, 34, 36, 37, 41, 45, 52, 53, 58, 59, 63, 64, 66, 71, 72, 76, 77, 83, 86, 87, 88, 89, 92, 93, 94, 98, 99, 105, 108, 109, 110, 114, 122, 124, 125, 126, 129, 130, 131, 132, 135, 136, 137, 140, 141, 142, 145, 148, 152, 159, 161, 162, 163, 164, 165, 169, 175, 177, 178, 179, 180, 184, 190, 192, 193, 194, 198, 205, 207, 208, 209, 213, 220, 222, 223, 227, 232, 234, 235, 236, 237, 241, 246, 248, 249, 250, 254, 255, 263, 264, 267, 268, 269, 270, 271, 272, 273, 274, 277, 278, 279, 283, 284, 292, 295, 298, 299, 302, 303, 304, 308, 309, 315, 318, 319], "summary": {"covered_lines": 121, "num_statements": 121, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [1, 3, 4, 5, 7, 10, 12, 15, 16, 18, 32, 43, 61, 74, 96, 112, 150, 167, 182, 196, 211, 225, 239, 252, 281, 306], "summary": {"covered_lines": 24, "num_statements": 24, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "tests/test_turborepo_migrate.py": {"executed_lines": [1, 3, 4, 5, 7, 10, 12, 15, 16, 19, 29, 32, 33, 35, 36, 37, 53, 56, 57, 59, 60, 61, 75, 77, 80, 81, 83, 85, 91, 92, 93, 95, 97, 98, 100, 102, 104, 105, 107, 109, 110, 112, 114, 115, 117, 119, 120, 122, 124, 125, 127, 129, 130, 132, 133, 134, 136, 139, 145, 150, 151, 153, 154, 156, 158, 159, 160, 162, 164, 165, 166, 168, 170, 171, 172, 174, 177, 178, 179, 182, 183, 185, 187, 188, 189, 191, 194, 195, 196, 198, 200, 201, 202, 204, 205, 207, 208, 211, 212, 213, 216, 217, 220, 226, 228, 229, 230, 232, 235, 236, 239, 240, 241, 244, 246, 247, 248, 250, 251, 252, 254, 256, 257, 258, 259, 260, 263, 264, 266, 268, 269, 270, 272, 273, 274, 276, 279, 282, 283, 285, 287, 288, 289, 291, 292, 293, 295, 298, 301, 302, 303, 304, 307, 308, 309, 311, 313, 314, 317, 320, 321, 322, 324, 326, 327, 330, 332, 333, 334, 335, 336, 339, 340, 341, 342, 344, 346, 351, 352, 354, 355, 357, 358, 359, 360, 362, 365, 370, 373, 374], "summary": {"covered_lines": 188, "num_statements": 188, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": [], "functions": {"mock_monorepo": {"executed_lines": [19, 29, 32, 33, 35, 36, 37, 53, 56, 57, 59, 60, 61, 75, 77], "summary": {"covered_lines": 15, "num_statements": 15, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTurborepoMigrator.test_init": {"executed_lines": [85, 91, 92, 93], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTurborepoMigrator.test_validate_path_exists": {"executed_lines": [97, 98], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTurborepoMigrator.test_validate_path_not_exists": {"executed_lines": [102, 104, 105], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTurborepoMigrator.test_validate_path_not_directory": {"executed_lines": [109, 110, 112, 114, 115], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTurborepoMigrator.test_validate_path_no_package_json": {"executed_lines": [119, 120, 122, 124, 125], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTurborepoMigrator.test_analyze_workspace_npm": {"executed_lines": [129, 130, 132, 133, 134], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTurborepoMigrator.test_analyze_workspace_pnpm": {"executed_lines": [139, 145, 150, 151, 153, 154], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTurborepoMigrator.test_discover_packages": {"executed_lines": [158, 159, 160, 162, 164, 165, 166], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTurborepoMigrator.test_analyze_scripts": {"executed_lines": [170, 171, 172, 174, 177, 178, 179, 182, 183], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTurborepoMigrator.test_infer_build_outputs": {"executed_lines": [187, 188, 189, 191, 194, 195, 196], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTurborepoMigrator.test_generate_turbo_config": {"executed_lines": [200, 201, 202, 204, 205, 207, 208, 211, 212, 213, 216, 217, 220], "summary": {"covered_lines": 13, "num_statements": 13, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTurborepoMigrator.test_update_root_package_json": {"executed_lines": [228, 229, 230, 232, 235, 236, 239, 240, 241], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTurborepoMigrator.test_generate_migration_report": {"executed_lines": [246, 247, 248, 250, 251, 252, 254, 256, 257, 258, 259, 260, 263, 264], "summary": {"covered_lines": 14, "num_statements": 14, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTurborepoMigrator.test_write_files_dry_run": {"executed_lines": [268, 269, 270, 272, 273, 274, 276, 279, 282, 283], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTurborepoMigrator.test_write_files_actual": {"executed_lines": [287, 288, 289, 291, 292, 293, 295, 298, 301, 302, 303, 304, 307, 308, 309], "summary": {"covered_lines": 15, "num_statements": 15, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTurborepoMigrator.test_full_migration_dry_run": {"executed_lines": [313, 314, 317, 320, 321, 322], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTurborepoMigrator.test_full_migration_actual": {"executed_lines": [326, 327, 330, 332, 333, 334, 335, 336, 339, 340, 341, 342], "summary": {"covered_lines": 12, "num_statements": 12, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTurborepoMigrator.test_parse_pnpm_workspace": {"executed_lines": [346, 351, 352, 354, 355, 357, 358, 359, 360], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TestTurborepoMigrator.test_monorepo_without_workspaces": {"executed_lines": [365, 370, 373, 374], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [1, 3, 4, 5, 7, 10, 12, 15, 16, 80, 81, 83, 95, 100, 107, 117, 127, 136, 156, 168, 185, 198, 226, 244, 266, 285, 311, 324, 344, 362], "summary": {"covered_lines": 28, "num_statements": 28, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"TestTurborepoMigrator": {"executed_lines": [85, 91, 92, 93, 97, 98, 102, 104, 105, 109, 110, 112, 114, 115, 119, 120, 122, 124, 125, 129, 130, 132, 133, 134, 139, 145, 150, 151, 153, 154, 158, 159, 160, 162, 164, 165, 166, 170, 171, 172, 174, 177, 178, 179, 182, 183, 187, 188, 189, 191, 194, 195, 196, 200, 201, 202, 204, 205, 207, 208, 211, 212, 213, 216, 217, 220, 228, 229, 230, 232, 235, 236, 239, 240, 241, 246, 247, 248, 250, 251, 252, 254, 256, 257, 258, 259, 260, 263, 264, 268, 269, 270, 272, 273, 274, 276, 279, 282, 283, 287, 288, 289, 291, 292, 293, 295, 298, 301, 302, 303, 304, 307, 308, 309, 313, 314, 317, 320, 321, 322, 326, 327, 330, 332, 333, 334, 335, 336, 339, 340, 341, 342, 346, 351, 352, 354, 355, 357, 358, 359, 360, 365, 370, 373, 374], "summary": {"covered_lines": 145, "num_statements": 145, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [1, 3, 4, 5, 7, 10, 12, 15, 16, 19, 29, 32, 33, 35, 36, 37, 53, 56, 57, 59, 60, 61, 75, 77, 80, 81, 83, 95, 100, 107, 117, 127, 136, 156, 168, 185, 198, 226, 244, 266, 285, 311, 324, 344, 362], "summary": {"covered_lines": 43, "num_statements": 43, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "turborepo_migrate.py": {"executed_lines": [2, 8, 9, 10, 11, 12, 13, 16, 17, 19, 33, 34, 35, 36, 37, 39, 41, 42, 44, 45, 47, 48, 49, 53, 55, 57, 58, 59, 62, 63, 67, 68, 73, 77, 79, 81, 83, 84, 85, 93, 94, 96, 97, 98, 100, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 115, 117, 120, 122, 123, 124, 126, 127, 128, 130, 140, 142, 144, 146, 147, 148, 149, 150, 152, 158, 159, 160, 162, 164, 166, 168, 171, 172, 178, 179, 185, 186, 189, 194, 199, 202, 208, 210, 212, 214, 215, 218, 219, 220, 222, 223, 224, 225, 228, 230, 232, 234, 235, 236, 239, 240, 242, 245, 248, 249, 251, 252, 254, 256, 260, 262, 263, 264, 265, 267, 268, 269, 270, 272, 273, 274, 275, 276, 278, 279, 280, 281, 282, 283, 285, 286, 287, 288, 290, 291, 292, 293, 294, 295, 296, 298, 299, 301, 302, 303, 305, 306, 308, 310, 312, 313, 314, 315, 316, 318, 321, 322, 323, 324, 327, 328, 329, 330, 332, 334, 335, 336, 337, 339, 340, 341, 343, 344, 345, 347, 348, 350, 351, 352, 354, 355, 356, 359, 393], "summary": {"covered_lines": 194, "num_statements": 213, "percent_covered": 91.07981220657277, "percent_covered_display": "91", "missing_lines": 19, "excluded_lines": 0}, "missing_lines": [86, 89, 90, 190, 191, 195, 196, 200, 221, 226, 246, 361, 364, 370, 375, 382, 384, 390, 394], "excluded_lines": [], "functions": {"TurborepoMigrator.__init__": {"executed_lines": [33, 34, 35, 36, 37], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TurborepoMigrator.validate_path": {"executed_lines": [41, 42, 44, 45, 47, 48, 49], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TurborepoMigrator.analyze_workspace": {"executed_lines": [55, 57, 58, 59, 62, 63, 67, 68, 73, 77], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TurborepoMigrator.discover_packages": {"executed_lines": [81, 83, 84, 85, 93, 94, 96, 97, 98], "summary": {"covered_lines": 9, "num_statements": 12, "percent_covered": 75.0, "percent_covered_display": "75", "missing_lines": 3, "excluded_lines": 0}, "missing_lines": [86, 89, 90], "excluded_lines": []}, "TurborepoMigrator._parse_pnpm_workspace": {"executed_lines": [102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113], "summary": {"covered_lines": 12, "num_statements": 12, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TurborepoMigrator._find_packages_by_pattern": {"executed_lines": [117, 120, 122, 123, 124, 126, 127, 128, 130], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TurborepoMigrator.analyze_scripts": {"executed_lines": [142, 144, 146, 147, 148, 149, 150, 152, 158, 159, 160, 162], "summary": {"covered_lines": 12, "num_statements": 12, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TurborepoMigrator.generate_turbo_config": {"executed_lines": [166, 168, 171, 172, 178, 179, 185, 186, 189, 194, 199, 202, 208], "summary": {"covered_lines": 13, "num_statements": 18, "percent_covered": 72.22222222222223, "percent_covered_display": "72", "missing_lines": 5, "excluded_lines": 0}, "missing_lines": [190, 191, 195, 196, 200], "excluded_lines": []}, "TurborepoMigrator._infer_build_outputs": {"executed_lines": [212, 214, 215, 218, 219, 220, 222, 223, 224, 225, 228], "summary": {"covered_lines": 11, "num_statements": 13, "percent_covered": 84.61538461538461, "percent_covered_display": "85", "missing_lines": 2, "excluded_lines": 0}, "missing_lines": [221, 226], "excluded_lines": []}, "TurborepoMigrator.update_root_package_json": {"executed_lines": [232, 234, 235, 236, 239, 240, 242, 245, 248, 249, 251, 252, 254], "summary": {"covered_lines": 13, "num_statements": 14, "percent_covered": 92.85714285714286, "percent_covered_display": "93", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [246], "excluded_lines": []}, "TurborepoMigrator.generate_migration_report": {"executed_lines": [260, 262, 263, 264, 265, 267, 268, 269, 270, 272, 273, 274, 275, 276, 278, 279, 280, 281, 282, 283, 285, 286, 287, 288, 290, 291, 292, 293, 294, 295, 296, 298, 299, 301, 302, 303, 305, 306, 308], "summary": {"covered_lines": 39, "num_statements": 39, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TurborepoMigrator.write_files": {"executed_lines": [312, 313, 314, 315, 316, 318, 321, 322, 323, 324, 327, 328, 329, 330], "summary": {"covered_lines": 14, "num_statements": 14, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TurborepoMigrator.migrate": {"executed_lines": [334, 335, 336, 337, 339, 340, 341, 343, 344, 345, 347, 348, 350, 351, 352, 354, 355, 356], "summary": {"covered_lines": 18, "num_statements": 18, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "main": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 7, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 7, "excluded_lines": 0}, "missing_lines": [361, 364, 370, 375, 382, 384, 390], "excluded_lines": []}, "": {"executed_lines": [2, 8, 9, 10, 11, 12, 13, 16, 17, 19, 39, 53, 79, 100, 115, 140, 164, 210, 230, 256, 310, 332, 359, 393], "summary": {"covered_lines": 22, "num_statements": 23, "percent_covered": 95.65217391304348, "percent_covered_display": "96", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [394], "excluded_lines": []}}, "classes": {"TurborepoMigrator": {"executed_lines": [33, 34, 35, 36, 37, 41, 42, 44, 45, 47, 48, 49, 55, 57, 58, 59, 62, 63, 67, 68, 73, 77, 81, 83, 84, 85, 93, 94, 96, 97, 98, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 117, 120, 122, 123, 124, 126, 127, 128, 130, 142, 144, 146, 147, 148, 149, 150, 152, 158, 159, 160, 162, 166, 168, 171, 172, 178, 179, 185, 186, 189, 194, 199, 202, 208, 212, 214, 215, 218, 219, 220, 222, 223, 224, 225, 228, 232, 234, 235, 236, 239, 240, 242, 245, 248, 249, 251, 252, 254, 260, 262, 263, 264, 265, 267, 268, 269, 270, 272, 273, 274, 275, 276, 278, 279, 280, 281, 282, 283, 285, 286, 287, 288, 290, 291, 292, 293, 294, 295, 296, 298, 299, 301, 302, 303, 305, 306, 308, 312, 313, 314, 315, 316, 318, 321, 322, 323, 324, 327, 328, 329, 330, 334, 335, 336, 337, 339, 340, 341, 343, 344, 345, 347, 348, 350, 351, 352, 354, 355, 356], "summary": {"covered_lines": 172, "num_statements": 183, "percent_covered": 93.98907103825137, "percent_covered_display": "94", "missing_lines": 11, "excluded_lines": 0}, "missing_lines": [86, 89, 90, 190, 191, 195, 196, 200, 221, 226, 246], "excluded_lines": []}, "": {"executed_lines": [2, 8, 9, 10, 11, 12, 13, 16, 17, 19, 39, 53, 79, 100, 115, 140, 164, 210, 230, 256, 310, 332, 359, 393], "summary": {"covered_lines": 22, "num_statements": 30, "percent_covered": 73.33333333333333, "percent_covered_display": "73", "missing_lines": 8, "excluded_lines": 0}, "missing_lines": [361, 364, 370, 375, 382, 384, 390, 394], "excluded_lines": []}}}}, "totals": {"covered_lines": 673, "num_statements": 708, "percent_covered": 95.05649717514125, "percent_covered_display": "95", "missing_lines": 35, "excluded_lines": 0}} -
requirements.txt 52 B
pytest>=7.0.0 pytest-cov>=4.0.0 pytest-mock>=3.10.0 -
test_nextjs_init.py 10.5 KB
"""Tests for nextjs-init.py script.""" import json import sys from pathlib import Path import pytest # Add parent directory to path to import the script sys.path.insert(0, str(Path(__file__).parent.parent)) from nextjs_init import NextJSInitializer class TestNextJSInitializer: """Test suite for NextJSInitializer.""" def test_init_with_defaults(self, tmp_path): """Test initialization with default parameters.""" initializer = NextJSInitializer( name="test-app", directory=tmp_path / "test-app" ) assert initializer.name == "test-app" assert initializer.typescript is True assert initializer.app_router is True assert initializer.src_dir is False assert initializer.tailwind is False assert initializer.eslint is True def test_validate_name_valid(self, tmp_path): """Test name validation with valid names.""" valid_names = ["my-app", "my_app", "myapp123", "test-app-1"] for name in valid_names: initializer = NextJSInitializer( name=name, directory=tmp_path / name ) initializer.validate_name() # Should not raise def test_validate_name_invalid(self, tmp_path): """Test name validation with invalid names.""" invalid_cases = [ ("", ValueError, "empty"), ("123app", ValueError, "starts with number"), ("my app", ValueError, "contains space"), ("my@app", ValueError, "contains special char"), ] for name, expected_error, reason in invalid_cases: initializer = NextJSInitializer( name=name, directory=tmp_path / (name or "empty") ) with pytest.raises(expected_error): initializer.validate_name() def test_check_directory_exists(self, tmp_path): """Test directory existence check.""" existing_dir = tmp_path / "existing" existing_dir.mkdir() initializer = NextJSInitializer( name="test-app", directory=existing_dir ) with pytest.raises(FileExistsError): initializer.check_directory() def test_create_directory_structure_app_router(self, tmp_path): """Test directory structure creation with App Router.""" project_dir = tmp_path / "test-app" initializer = NextJSInitializer( name="test-app", directory=project_dir, app_router=True ) initializer.create_directory_structure() # Check directories assert (project_dir / "app").exists() assert (project_dir / "public").exists() assert (project_dir / "components").exists() assert (project_dir / "lib").exists() # Check App Router files assert (project_dir / "app" / "layout.tsx").exists() assert (project_dir / "app" / "page.tsx").exists() assert (project_dir / "app" / "globals.css").exists() def test_create_directory_structure_with_src(self, tmp_path): """Test directory structure with src/ directory.""" project_dir = tmp_path / "test-app" initializer = NextJSInitializer( name="test-app", directory=project_dir, src_dir=True ) initializer.create_directory_structure() # Check src structure assert (project_dir / "src" / "app").exists() assert (project_dir / "src" / "components").exists() assert (project_dir / "src" / "lib").exists() def test_package_json_generation(self, tmp_path): """Test package.json generation.""" initializer = NextJSInitializer( name="test-app", directory=tmp_path / "test-app", typescript=True, tailwind=True, eslint=True ) package_json = initializer._get_package_json() assert package_json["name"] == "test-app" assert package_json["version"] == "0.1.0" assert package_json["private"] is True # Check scripts assert "dev" in package_json["scripts"] assert "build" in package_json["scripts"] assert "start" in package_json["scripts"] assert "lint" in package_json["scripts"] # Check dependencies assert "next" in package_json["dependencies"] assert "react" in package_json["dependencies"] assert "react-dom" in package_json["dependencies"] # Check TypeScript dependencies assert "typescript" in package_json["devDependencies"] assert "@types/node" in package_json["devDependencies"] assert "@types/react" in package_json["devDependencies"] # Check Tailwind dependencies assert "tailwindcss" in package_json["dependencies"] # Check ESLint dependencies assert "eslint" in package_json["devDependencies"] def test_tsconfig_generation(self, tmp_path): """Test tsconfig.json generation.""" initializer = NextJSInitializer( name="test-app", directory=tmp_path / "test-app", typescript=True, import_alias="@/*" ) tsconfig = initializer._get_tsconfig() assert "compilerOptions" in tsconfig assert tsconfig["compilerOptions"]["strict"] is True assert tsconfig["compilerOptions"]["jsx"] == "preserve" assert "@/*" in tsconfig["compilerOptions"]["paths"] assert "next-env.d.ts" in tsconfig["include"] def test_layout_content_typescript(self, tmp_path): """Test layout.tsx content generation.""" initializer = NextJSInitializer( name="test-app", directory=tmp_path / "test-app", typescript=True ) content = initializer._get_layout_content() assert "import './globals.css'" in content assert "export const metadata" in content assert "children: React.ReactNode" in content assert "<html lang=\"en\">" in content def test_layout_content_javascript(self, tmp_path): """Test layout.jsx content generation.""" initializer = NextJSInitializer( name="test-app", directory=tmp_path / "test-app", typescript=False ) content = initializer._get_layout_content() assert "import './globals.css'" in content assert "export const metadata" in content assert "React.ReactNode" not in content # No TypeScript types def test_tailwind_config_typescript(self, tmp_path): """Test Tailwind config generation with TypeScript.""" initializer = NextJSInitializer( name="test-app", directory=tmp_path / "test-app", typescript=True, tailwind=True ) config = initializer._get_tailwind_config() assert "import type { Config }" in config assert "const config: Config" in config assert "content:" in config def test_tailwind_config_javascript(self, tmp_path): """Test Tailwind config generation with JavaScript.""" initializer = NextJSInitializer( name="test-app", directory=tmp_path / "test-app", typescript=False, tailwind=True ) config = initializer._get_tailwind_config() assert "module.exports" in config assert "content:" in config def test_gitignore_generation(self, tmp_path): """Test .gitignore generation.""" initializer = NextJSInitializer( name="test-app", directory=tmp_path / "test-app" ) gitignore = initializer._get_gitignore() assert "/node_modules" in gitignore assert "/.next/" in gitignore assert ".env*.local" in gitignore assert ".DS_Store" in gitignore def test_readme_generation(self, tmp_path): """Test README.md generation.""" initializer = NextJSInitializer( name="test-app", directory=tmp_path / "test-app" ) readme = initializer._get_readme() assert "# test-app" in readme assert "Next.js" in readme assert "npm run dev" in readme def test_create_config_files(self, tmp_path): """Test configuration files creation.""" project_dir = tmp_path / "test-app" initializer = NextJSInitializer( name="test-app", directory=project_dir, typescript=True, tailwind=True, eslint=True ) initializer.create_directory_structure() initializer.create_config_files() # Check all config files exist assert (project_dir / "package.json").exists() assert (project_dir / "next.config.js").exists() assert (project_dir / "tsconfig.json").exists() assert (project_dir / ".eslintrc.json").exists() assert (project_dir / "tailwind.config.ts").exists() assert (project_dir / "postcss.config.js").exists() assert (project_dir / ".gitignore").exists() assert (project_dir / "README.md").exists() # Verify package.json is valid JSON with open(project_dir / "package.json") as f: package_json = json.load(f) assert package_json["name"] == "test-app" def test_full_initialization(self, tmp_path): """Test full initialization process.""" project_dir = tmp_path / "test-app" initializer = NextJSInitializer( name="test-app", directory=project_dir, typescript=True, app_router=True, tailwind=True ) initializer.initialize() # Verify directory exists assert project_dir.exists() # Verify structure assert (project_dir / "app").exists() assert (project_dir / "public").exists() # Verify config files assert (project_dir / "package.json").exists() assert (project_dir / "tsconfig.json").exists() assert (project_dir / "next.config.js").exists() def test_pages_router_structure(self, tmp_path): """Test Pages Router directory structure.""" project_dir = tmp_path / "test-app" initializer = NextJSInitializer( name="test-app", directory=project_dir, app_router=False # Use Pages Router ) initializer.create_directory_structure() # Check Pages Router files assert (project_dir / "pages" / "_app.tsx").exists() assert (project_dir / "pages" / "index.tsx").exists() -
test_turborepo_migrate.py 12.6 KB
"""Tests for turborepo-migrate.py script.""" import json import sys from pathlib import Path import pytest # Add parent directory to path to import the script sys.path.insert(0, str(Path(__file__).parent.parent)) from turborepo_migrate import TurborepoMigrator @pytest.fixture def mock_monorepo(tmp_path): """Create a mock monorepo structure.""" # Root package.json root_pkg = { "name": "test-monorepo", "private": True, "workspaces": ["apps/*", "packages/*"], "scripts": { "build": "npm run build --workspaces", "test": "npm run test --workspaces" } } (tmp_path / "package.json").write_text(json.dumps(root_pkg, indent=2)) # Create apps apps_dir = tmp_path / "apps" apps_dir.mkdir() web_dir = apps_dir / "web" web_dir.mkdir() (web_dir / "package.json").write_text(json.dumps({ "name": "web", "version": "1.0.0", "scripts": { "dev": "next dev", "build": "next build", "test": "jest", "lint": "eslint ." }, "dependencies": { "@repo/ui": "*", "next": "latest" } }, indent=2)) # Create Next.js output directory (web_dir / ".next").mkdir() # Create packages packages_dir = tmp_path / "packages" packages_dir.mkdir() ui_dir = packages_dir / "ui" ui_dir.mkdir() (ui_dir / "package.json").write_text(json.dumps({ "name": "@repo/ui", "version": "0.0.0", "scripts": { "build": "tsc", "test": "jest", "lint": "eslint ." }, "dependencies": { "react": "latest" } }, indent=2)) # Create dist directory (ui_dir / "dist").mkdir() return tmp_path class TestTurborepoMigrator: """Test suite for TurborepoMigrator.""" def test_init(self, tmp_path): """Test migrator initialization.""" migrator = TurborepoMigrator( path=tmp_path, dry_run=True, package_manager="npm" ) assert migrator.path == tmp_path.resolve() assert migrator.dry_run is True assert migrator.package_manager == "npm" def test_validate_path_exists(self, mock_monorepo): """Test path validation with valid monorepo.""" migrator = TurborepoMigrator(path=mock_monorepo) migrator.validate_path() # Should not raise def test_validate_path_not_exists(self, tmp_path): """Test path validation with non-existent path.""" migrator = TurborepoMigrator(path=tmp_path / "nonexistent") with pytest.raises(FileNotFoundError): migrator.validate_path() def test_validate_path_not_directory(self, tmp_path): """Test path validation with file instead of directory.""" file_path = tmp_path / "file.txt" file_path.touch() migrator = TurborepoMigrator(path=file_path) with pytest.raises(NotADirectoryError): migrator.validate_path() def test_validate_path_no_package_json(self, tmp_path): """Test path validation without package.json.""" empty_dir = tmp_path / "empty" empty_dir.mkdir() migrator = TurborepoMigrator(path=empty_dir) with pytest.raises(FileNotFoundError): migrator.validate_path() def test_analyze_workspace_npm(self, mock_monorepo): """Test workspace analysis for npm/yarn workspaces.""" migrator = TurborepoMigrator(path=mock_monorepo) migrator.analyze_workspace() assert migrator.workspace_config["type"] == "npm/yarn" assert "apps/*" in migrator.workspace_config["patterns"] assert "packages/*" in migrator.workspace_config["patterns"] def test_analyze_workspace_pnpm(self, tmp_path): """Test workspace analysis for pnpm workspaces.""" # Create root package.json without workspaces (tmp_path / "package.json").write_text(json.dumps({ "name": "test-monorepo", "private": True })) # Create pnpm-workspace.yaml (tmp_path / "pnpm-workspace.yaml").write_text("""packages: - 'apps/*' - 'packages/*' """) migrator = TurborepoMigrator(path=tmp_path) migrator.analyze_workspace() assert migrator.workspace_config["type"] == "pnpm" assert migrator.workspace_config["file"] == "pnpm-workspace.yaml" def test_discover_packages(self, mock_monorepo): """Test package discovery.""" migrator = TurborepoMigrator(path=mock_monorepo) migrator.analyze_workspace() migrator.discover_packages() assert len(migrator.packages) == 2 package_names = {pkg["name"] for pkg in migrator.packages} assert "web" in package_names assert "@repo/ui" in package_names def test_analyze_scripts(self, mock_monorepo): """Test script analysis.""" migrator = TurborepoMigrator(path=mock_monorepo) migrator.analyze_workspace() migrator.discover_packages() common_scripts = migrator.analyze_scripts() # All packages have build, test, lint assert "build" in common_scripts assert "test" in common_scripts assert "lint" in common_scripts # Check package counts assert len(common_scripts["build"]) == 2 assert len(common_scripts["test"]) == 2 def test_infer_build_outputs(self, mock_monorepo): """Test build output inference.""" migrator = TurborepoMigrator(path=mock_monorepo) migrator.analyze_workspace() migrator.discover_packages() outputs = migrator._infer_build_outputs() # Should detect .next and dist directories assert ".next/**" in outputs assert "!.next/cache/**" in outputs assert "dist/**" in outputs def test_generate_turbo_config(self, mock_monorepo): """Test turbo.json generation.""" migrator = TurborepoMigrator(path=mock_monorepo) migrator.analyze_workspace() migrator.discover_packages() common_scripts = migrator.analyze_scripts() turbo_config = migrator.generate_turbo_config(common_scripts) assert "$schema" in turbo_config assert "pipeline" in turbo_config # Check build task assert "build" in turbo_config["pipeline"] assert turbo_config["pipeline"]["build"]["dependsOn"] == ["^build"] assert "outputs" in turbo_config["pipeline"]["build"] # Check test task assert "test" in turbo_config["pipeline"] assert "coverage/**" in turbo_config["pipeline"]["test"]["outputs"] # Check lint task assert "lint" in turbo_config["pipeline"] # Note: dev task won't be in pipeline because it's only in 1 package # (needs to be in 2+ packages to be considered "common") # This is correct behavior - only truly common scripts are included def test_update_root_package_json(self, mock_monorepo): """Test root package.json update.""" migrator = TurborepoMigrator(path=mock_monorepo) migrator.analyze_workspace() migrator.discover_packages() updated_package_json = migrator.update_root_package_json() # Check turbo added to devDependencies assert "turbo" in updated_package_json["devDependencies"] assert updated_package_json["devDependencies"]["turbo"] == "latest" # Check scripts updated (only common scripts are added) assert updated_package_json["scripts"]["build"] == "turbo run build" assert updated_package_json["scripts"]["test"] == "turbo run test" assert updated_package_json["scripts"]["lint"] == "turbo run lint" # dev is only in one package, so it won't be added def test_generate_migration_report(self, mock_monorepo): """Test migration report generation.""" migrator = TurborepoMigrator(path=mock_monorepo) migrator.analyze_workspace() migrator.discover_packages() common_scripts = migrator.analyze_scripts() turbo_config = migrator.generate_turbo_config(common_scripts) updated_package_json = migrator.update_root_package_json() report = migrator.generate_migration_report(turbo_config, updated_package_json) assert "TURBOREPO MIGRATION REPORT" in report assert "PACKAGES:" in report assert "TURBO.JSON PIPELINE:" in report assert "ROOT PACKAGE.JSON SCRIPTS:" in report assert "RECOMMENDATIONS:" in report # Check package names appear assert "web" in report assert "@repo/ui" in report def test_write_files_dry_run(self, mock_monorepo, capsys): """Test file writing in dry-run mode.""" migrator = TurborepoMigrator(path=mock_monorepo, dry_run=True) migrator.analyze_workspace() migrator.discover_packages() common_scripts = migrator.analyze_scripts() turbo_config = migrator.generate_turbo_config(common_scripts) updated_package_json = migrator.update_root_package_json() migrator.write_files(turbo_config, updated_package_json) # Check files not created assert not (mock_monorepo / "turbo.json").exists() # Check output captured = capsys.readouterr() assert "DRY RUN" in captured.out def test_write_files_actual(self, mock_monorepo): """Test actual file writing.""" migrator = TurborepoMigrator(path=mock_monorepo, dry_run=False) migrator.analyze_workspace() migrator.discover_packages() common_scripts = migrator.analyze_scripts() turbo_config = migrator.generate_turbo_config(common_scripts) updated_package_json = migrator.update_root_package_json() migrator.write_files(turbo_config, updated_package_json) # Check turbo.json created assert (mock_monorepo / "turbo.json").exists() # Verify content with open(mock_monorepo / "turbo.json") as f: saved_config = json.load(f) assert saved_config["$schema"] == turbo_config["$schema"] assert "pipeline" in saved_config # Check package.json updated with open(mock_monorepo / "package.json") as f: saved_package = json.load(f) assert "turbo" in saved_package["devDependencies"] def test_full_migration_dry_run(self, mock_monorepo): """Test full migration process in dry-run mode.""" migrator = TurborepoMigrator(path=mock_monorepo, dry_run=True) migrator.migrate() # Files should not be created in dry-run assert not (mock_monorepo / "turbo.json").exists() # Original package.json should be unchanged with open(mock_monorepo / "package.json") as f: package_json = json.load(f) assert "turbo" not in package_json.get("devDependencies", {}) def test_full_migration_actual(self, mock_monorepo): """Test full migration process.""" migrator = TurborepoMigrator(path=mock_monorepo, dry_run=False) migrator.migrate() # Check turbo.json created assert (mock_monorepo / "turbo.json").exists() with open(mock_monorepo / "turbo.json") as f: turbo_config = json.load(f) assert "$schema" in turbo_config assert "pipeline" in turbo_config assert "build" in turbo_config["pipeline"] # Check package.json updated with open(mock_monorepo / "package.json") as f: package_json = json.load(f) assert "turbo" in package_json["devDependencies"] assert package_json["scripts"]["build"] == "turbo run build" def test_parse_pnpm_workspace(self, tmp_path): """Test pnpm-workspace.yaml parsing.""" yaml_content = """packages: - 'apps/*' - 'packages/*' - 'tools/*' """ yaml_file = tmp_path / "pnpm-workspace.yaml" yaml_file.write_text(yaml_content) migrator = TurborepoMigrator(path=tmp_path) patterns = migrator._parse_pnpm_workspace(yaml_file) assert len(patterns) == 3 assert "apps/*" in patterns assert "packages/*" in patterns assert "tools/*" in patterns def test_monorepo_without_workspaces(self, tmp_path): """Test migration fails for non-workspace monorepo.""" # Create package.json without workspaces (tmp_path / "package.json").write_text(json.dumps({ "name": "not-a-monorepo", "version": "1.0.0" })) migrator = TurborepoMigrator(path=tmp_path) # migrate() calls sys.exit(1) on error, so we catch SystemExit with pytest.raises(SystemExit): migrator.migrate()
-
-
nextjs_init.py 14.5 KB
#!/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() -
requirements.txt 430 B
# Web Frameworks Skill Dependencies # Python 3.10+ required # No Python package dependencies - uses only standard library # Testing dependencies (dev) pytest>=8.0.0 pytest-cov>=4.1.0 pytest-mock>=3.12.0 # Note: This skill works with Node.js frameworks # Requires Node.js and package managers: # - Node.js 18+: https://nodejs.org/ # - npm (comes with Node.js) # - pnpm: npm install -g pnpm # - yarn: npm install -g yarn -
turborepo_migrate.py 12.9 KB
#!/usr/bin/env python3 """ Turborepo Migration Script Convert existing monorepo to Turborepo with intelligent pipeline generation. """ import argparse import json import os import sys from pathlib import Path from typing import Dict, List, Optional, Set class TurborepoMigrator: """Migrate existing monorepo to Turborepo.""" def __init__( self, path: Path, dry_run: bool = False, package_manager: str = "npm", ): """ Initialize TurborepoMigrator. Args: path: Path to existing monorepo dry_run: Preview changes without writing files package_manager: Package manager (npm, yarn, pnpm, bun) """ self.path = path.resolve() self.dry_run = dry_run self.package_manager = package_manager self.packages: List[Dict] = [] self.workspace_config: Dict = {} def validate_path(self) -> None: """Validate monorepo path.""" if not self.path.exists(): raise FileNotFoundError(f"Path '{self.path}' does not exist") if not self.path.is_dir(): raise NotADirectoryError(f"Path '{self.path}' is not a directory") package_json = self.path / "package.json" if not package_json.exists(): raise FileNotFoundError( f"No package.json found in '{self.path}'. Not a valid monorepo." ) def analyze_workspace(self) -> None: """Analyze existing workspace configuration.""" print("Analyzing workspace...") package_json = self.path / "package.json" with open(package_json) as f: root_config = json.load(f) # Detect workspace configuration if "workspaces" in root_config: self.workspace_config = { "type": "npm/yarn", "patterns": root_config["workspaces"], } elif (self.path / "pnpm-workspace.yaml").exists(): self.workspace_config = { "type": "pnpm", "file": "pnpm-workspace.yaml", } else: raise ValueError( "No workspace configuration found. Monorepo structure not detected." ) print(f" Workspace type: {self.workspace_config['type']}") def discover_packages(self) -> None: """Discover all packages in workspace.""" print("Discovering packages...") if self.workspace_config["type"] == "npm/yarn": patterns = self.workspace_config["patterns"] if isinstance(patterns, dict): patterns = patterns.get("packages", []) else: # Parse pnpm-workspace.yaml yaml_file = self.path / "pnpm-workspace.yaml" patterns = self._parse_pnpm_workspace(yaml_file) # Find all packages matching patterns for pattern in patterns: self._find_packages_by_pattern(pattern) print(f" Found {len(self.packages)} packages") for pkg in self.packages: print(f" - {pkg['name']} ({pkg['path'].relative_to(self.path)})") def _parse_pnpm_workspace(self, yaml_file: Path) -> List[str]: """Parse pnpm-workspace.yaml file.""" patterns = [] with open(yaml_file) as f: in_packages = False for line in f: line = line.strip() if line.startswith("packages:"): in_packages = True continue if in_packages and line.startswith("- "): pattern = line[2:].strip().strip("'\"") patterns.append(pattern) return patterns def _find_packages_by_pattern(self, pattern: str) -> None: """Find packages matching glob pattern.""" import glob # Convert pattern to absolute path search_pattern = str(self.path / pattern) for match in glob.glob(search_pattern): match_path = Path(match) package_json = match_path / "package.json" if package_json.exists(): with open(package_json) as f: pkg_data = json.load(f) self.packages.append( { "name": pkg_data.get("name", match_path.name), "path": match_path, "scripts": pkg_data.get("scripts", {}), "dependencies": pkg_data.get("dependencies", {}), "devDependencies": pkg_data.get("devDependencies", {}), } ) def analyze_scripts(self) -> Dict[str, Set[str]]: """Analyze common scripts across packages.""" print("Analyzing scripts...") script_map: Dict[str, Set[str]] = {} for pkg in self.packages: for script_name in pkg["scripts"]: if script_name not in script_map: script_map[script_name] = set() script_map[script_name].add(pkg["name"]) common_scripts = { name: packages for name, packages in script_map.items() if len(packages) >= 2 # Present in at least 2 packages } print(f" Found {len(common_scripts)} common scripts:") for script, packages in common_scripts.items(): print(f" - {script} ({len(packages)} packages)") return common_scripts def generate_turbo_config(self, common_scripts: Dict[str, Set[str]]) -> Dict: """Generate turbo.json configuration.""" print("Generating turbo.json configuration...") pipeline = {} # Build task if "build" in common_scripts: pipeline["build"] = { "dependsOn": ["^build"], "outputs": self._infer_build_outputs(), } # Test task if "test" in common_scripts: pipeline["test"] = { "dependsOn": ["build"], "outputs": ["coverage/**"], } # Lint task if "lint" in common_scripts: pipeline["lint"] = {"dependsOn": ["^build"]} # Typecheck task if "typecheck" in common_scripts or "type-check" in common_scripts: task_name = "typecheck" if "typecheck" in common_scripts else "type-check" pipeline[task_name] = {"dependsOn": ["^build"]} # Dev task if "dev" in common_scripts or "start" in common_scripts: dev_task = "dev" if "dev" in common_scripts else "start" pipeline[dev_task] = {"cache": False, "persistent": True} # Clean task if "clean" in common_scripts: pipeline["clean"] = {"cache": False} turbo_config = { "$schema": "https://turbo.build/schema.json", "globalDependencies": ["**/.env.*local"], "pipeline": pipeline, } return turbo_config def _infer_build_outputs(self) -> List[str]: """Infer build output directories from packages.""" outputs = set() for pkg in self.packages: pkg_path = pkg["path"] # Check common output directories if (pkg_path / "dist").exists(): outputs.add("dist/**") if (pkg_path / "build").exists(): outputs.add("build/**") if (pkg_path / ".next").exists(): outputs.add(".next/**") outputs.add("!.next/cache/**") if (pkg_path / "out").exists(): outputs.add("out/**") return sorted(list(outputs)) or ["dist/**"] def update_root_package_json(self) -> Dict: """Update root package.json with Turborepo scripts.""" print("Updating root package.json...") package_json_path = self.path / "package.json" with open(package_json_path) as f: package_json = json.load(f) # Add turbo to devDependencies if "devDependencies" not in package_json: package_json["devDependencies"] = {} package_json["devDependencies"]["turbo"] = "latest" # Update scripts to use turbo if "scripts" not in package_json: package_json["scripts"] = {} common_tasks = ["build", "dev", "test", "lint", "typecheck", "clean"] for task in common_tasks: # Check if task exists in any package if any(task in pkg["scripts"] for pkg in self.packages): package_json["scripts"][task] = f"turbo run {task}" return package_json def generate_migration_report( self, turbo_config: Dict, updated_package_json: Dict ) -> str: """Generate migration report.""" report = [] report.append("=" * 60) report.append("TURBOREPO MIGRATION REPORT") report.append("=" * 60) report.append("") report.append(f"Monorepo Path: {self.path}") report.append(f"Package Manager: {self.package_manager}") report.append(f"Total Packages: {len(self.packages)}") report.append("") report.append("PACKAGES:") for pkg in self.packages: rel_path = pkg["path"].relative_to(self.path) report.append(f" - {pkg['name']} ({rel_path})") report.append("") report.append("TURBO.JSON PIPELINE:") for task, config in turbo_config["pipeline"].items(): report.append(f" {task}:") for key, value in config.items(): report.append(f" {key}: {value}") report.append("") report.append("ROOT PACKAGE.JSON SCRIPTS:") for script, command in updated_package_json.get("scripts", {}).items(): report.append(f" {script}: {command}") report.append("") report.append("RECOMMENDATIONS:") report.append(" 1. Review generated turbo.json pipeline configuration") report.append(" 2. Adjust output directories based on your build tools") report.append(" 3. Configure remote caching: turbo login && turbo link") report.append(" 4. Run 'npm install' to install Turborepo") report.append(" 5. Test with: turbo run build --dry-run") report.append("") if self.dry_run: report.append("DRY RUN MODE: No files were modified") else: report.append("FILES CREATED/MODIFIED:") report.append(f" - {self.path / 'turbo.json'}") report.append(f" - {self.path / 'package.json'}") report.append("") report.append("=" * 60) return "\n".join(report) def write_files(self, turbo_config: Dict, updated_package_json: Dict) -> None: """Write configuration files.""" if self.dry_run: print("\nDRY RUN - Files that would be created/modified:") print(f" - {self.path / 'turbo.json'}") print(f" - {self.path / 'package.json'}") return print("Writing files...") # Write turbo.json turbo_json_path = self.path / "turbo.json" with open(turbo_json_path, "w") as f: json.dump(turbo_config, f, indent=2) print(f" ✓ Created {turbo_json_path}") # Write updated package.json package_json_path = self.path / "package.json" with open(package_json_path, "w") as f: json.dump(updated_package_json, f, indent=2) print(f" ✓ Updated {package_json_path}") def migrate(self) -> None: """Run migration process.""" try: print(f"Migrating monorepo to Turborepo: {self.path}") print(f"Dry run: {self.dry_run}") print() self.validate_path() self.analyze_workspace() self.discover_packages() common_scripts = self.analyze_scripts() turbo_config = self.generate_turbo_config(common_scripts) updated_package_json = self.update_root_package_json() print() self.write_files(turbo_config, updated_package_json) print() report = self.generate_migration_report(turbo_config, updated_package_json) print(report) except Exception as e: print(f"Error: {e}", file=sys.stderr) sys.exit(1) def main(): """CLI entry point.""" parser = argparse.ArgumentParser( description="Migrate existing monorepo to Turborepo" ) parser.add_argument( "--path", type=Path, default=Path.cwd(), help="Path to monorepo (default: current directory)", ) parser.add_argument( "--dry-run", action="store_true", help="Preview changes without writing files", ) parser.add_argument( "--package-manager", choices=["npm", "yarn", "pnpm", "bun"], default="npm", help="Package manager (default: npm)", ) args = parser.parse_args() migrator = TurborepoMigrator( path=args.path, dry_run=args.dry_run, package_manager=args.package_manager, ) migrator.migrate() if __name__ == "__main__": main() -
__init__.py 0 B
-
-
SKILL.md 9.2 KB
--- name: web-frameworks description: Build modern full-stack web applications with Next.js (App Router, Server Components, RSC, PPR, SSR, SSG, ISR), Turborepo (monorepo management, task pipelines, remote caching, parallel execution), and RemixIcon (3100+ SVG icons in outlined/filled styles). Use when creating React applications, implementing server-side rendering, setting up monorepos with multiple packages, optimizing build performance and caching strategies, adding icon libraries, managing shared dependencies, or working with TypeScript full-stack projects. license: MIT version: 1.0.0 --- # Web Frameworks Skill Group Comprehensive guide for building modern full-stack web applications using Next.js, Turborepo, and RemixIcon. ## Overview This skill group combines three powerful tools for web development: **Next.js** - React framework with SSR, SSG, RSC, and optimization features **Turborepo** - High-performance monorepo build system for JavaScript/TypeScript **RemixIcon** - Icon library with 3,100+ outlined and filled style icons ## When to Use This Skill Group - Building new full-stack web applications with modern React - Setting up monorepos with multiple apps and shared packages - Implementing server-side rendering and static generation - Optimizing build performance with intelligent caching - Creating consistent UI with professional iconography - Managing workspace dependencies across multiple projects - Deploying production-ready applications with proper optimization ## Stack Selection Guide ### Single Application: Next.js + RemixIcon Use when building a standalone application: - E-commerce sites - Marketing websites - SaaS applications - Documentation sites - Blogs and content platforms **Setup:** ```bash npx create-next-app@latest my-app cd my-app npm install remixicon ``` ### Monorepo: Next.js + Turborepo + RemixIcon Use when building multiple applications with shared code: - Microfrontends - Multi-tenant platforms - Internal tools with shared component library - Multiple apps (web, admin, mobile-web) sharing logic - Design system with documentation site **Setup:** ```bash npx create-turbo@latest my-monorepo # Then configure Next.js apps in apps/ directory # Install remixicon in shared UI packages ``` ### Framework Features Comparison | Feature | Next.js | Turborepo | RemixIcon | |---------|---------|-----------|-----------| | Primary Use | Web framework | Build system | UI icons | | Best For | SSR/SSG apps | Monorepos | Consistent iconography | | Performance | Built-in optimization | Caching & parallel tasks | Lightweight fonts/SVG | | TypeScript | Full support | Full support | Type definitions available | ## Quick Start ### Next.js Application ```bash # Create new project npx create-next-app@latest my-app cd my-app # Install RemixIcon npm install remixicon # Import in layout # app/layout.tsx import 'remixicon/fonts/remixicon.css' # Start development npm run dev ``` ### Turborepo Monorepo ```bash # Create monorepo npx create-turbo@latest my-monorepo cd my-monorepo # Structure: # apps/web/ - Next.js application # apps/docs/ - Documentation site # packages/ui/ - Shared components with RemixIcon # packages/config/ - Shared configs # turbo.json - Pipeline configuration # Run all apps npm run dev # Build all packages npm run build ``` ### RemixIcon Integration ```tsx // Webfont (HTML/CSS) <i className="ri-home-line"></i> <i className="ri-search-fill ri-2x"></i> // React component import { RiHomeLine, RiSearchFill } from "@remixicon/react" <RiHomeLine size={24} /> <RiSearchFill size={32} color="blue" /> ``` ## Reference Navigation **Next.js References:** - [App Router Architecture](./references/nextjs-app-router.md) - Routing, layouts, pages, parallel routes - [Server Components](./references/nextjs-server-components.md) - RSC patterns, client vs server, streaming - [Data Fetching](./references/nextjs-data-fetching.md) - fetch API, caching, revalidation, loading states - [Optimization](./references/nextjs-optimization.md) - Images, fonts, scripts, bundle analysis, PPR **Turborepo References:** - [Setup & Configuration](./references/turborepo-setup.md) - Installation, workspace config, package structure - [Task Pipelines](./references/turborepo-pipelines.md) - Dependencies, parallel execution, task ordering - [Caching Strategies](./references/turborepo-caching.md) - Local cache, remote cache, cache invalidation **RemixIcon References:** - [Integration Guide](./references/remix-icon-integration.md) - Installation, usage, customization, accessibility ## Common Patterns & Workflows ### Pattern 1: Full-Stack Monorepo ``` my-monorepo/ ├── apps/ │ ├── web/ # Customer-facing Next.js app │ ├── admin/ # Admin dashboard Next.js app │ └── docs/ # Documentation site ├── packages/ │ ├── ui/ # Shared UI with RemixIcon │ ├── api-client/ # API client library │ ├── config/ # ESLint, TypeScript configs │ └── types/ # Shared TypeScript types └── turbo.json # Build pipeline ``` **turbo.json:** ```json { "$schema": "https://turbo.build/schema.json", "pipeline": { "build": { "dependsOn": ["^build"], "outputs": [".next/**", "!.next/cache/**", "dist/**"] }, "dev": { "cache": false, "persistent": true }, "lint": {}, "test": { "dependsOn": ["build"] } } } ``` ### Pattern 2: Shared Component Library ```tsx // packages/ui/src/button.tsx import { RiLoader4Line } from "@remixicon/react" export function Button({ children, loading, icon }) { return ( <button> {loading ? <RiLoader4Line className="animate-spin" /> : icon} {children} </button> ) } // apps/web/app/page.tsx import { Button } from "@repo/ui/button" import { RiHomeLine } from "@remixicon/react" export default function Page() { return <Button icon={<RiHomeLine />}>Home</Button> } ``` ### Pattern 3: Optimized Data Fetching ```tsx // app/posts/[slug]/page.tsx import { notFound } from 'next/navigation' // Static generation at build time export async function generateStaticParams() { const posts = await getPosts() return posts.map(post => ({ slug: post.slug })) } // Revalidate every hour async function getPost(slug: string) { const res = await fetch(`https://api.example.com/posts/${slug}`, { next: { revalidate: 3600 } }) if (!res.ok) return null return res.json() } export default async function Post({ params }: { params: { slug: string } }) { const post = await getPost(params.slug) if (!post) notFound() return <article>{post.content}</article> } ``` ### Pattern 4: Monorepo CI/CD Pipeline ```yaml # .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: 18 - run: npm install - run: npx turbo run build test lint env: TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: ${{ secrets.TURBO_TEAM }} ``` ## Utility Scripts Python utilities in `scripts/` directory: **nextjs-init.py** - Initialize Next.js project with best practices **turborepo-migrate.py** - Convert existing monorepo to Turborepo Usage examples: ```bash # Initialize new Next.js app with TypeScript and recommended setup python scripts/nextjs-init.py --name my-app --typescript --app-router # Migrate existing monorepo to Turborepo with dry-run python scripts/turborepo-migrate.py --path ./my-monorepo --dry-run # Run tests cd scripts/tests pytest ``` ## Best Practices **Next.js:** - Default to Server Components, use Client Components only when needed - Implement proper loading and error states - Use Image component for automatic optimization - Set proper metadata for SEO - Leverage caching strategies (force-cache, revalidate, no-store) **Turborepo:** - Structure monorepo with clear separation (apps/, packages/) - Define task dependencies correctly (^build for topological) - Configure outputs for proper caching - Enable remote caching for team collaboration - Use filters to run tasks on changed packages only **RemixIcon:** - Use line style for minimal interfaces, fill for emphasis - Maintain 24x24 grid alignment for crisp rendering - Provide aria-labels for accessibility - Use currentColor for flexible theming - Prefer webfonts for multiple icons, SVG for single icons ## Resources - Next.js: https://nextjs.org/docs/llms.txt - Turborepo: https://turbo.build/repo/docs - RemixIcon: https://remixicon.com ## Implementation Checklist Building with this stack: - [ ] Create project structure (single app or monorepo) - [ ] Configure TypeScript and ESLint - [ ] Set up Next.js with App Router - [ ] Configure Turborepo pipeline (if monorepo) - [ ] Install and configure RemixIcon - [ ] Implement routing and layouts - [ ] Add loading and error states - [ ] Configure image and font optimization - [ ] Set up data fetching patterns - [ ] Configure caching strategies - [ ] Add API routes as needed - [ ] Implement shared component library (if monorepo) - [ ] Configure remote caching (if monorepo) - [ ] Set up CI/CD pipeline - [ ] Configure deployment platform
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.