.claude/skills/monorepo-pnpm-patterns/SKILL.md
Master pnpm workspace management for RidenDine monorepo. Use when: (1) adding new packages or apps, (2) managing dependencies across workspace, (3) debugging module resolution, (4) running tasks across packages, (5) optimizing builds. Key insight: pnpm workspaces with "workspace:*" protocol for internal packages, strict dependency hoisting disabled.
npx skillsauth add Ritenoob/ridedine monorepo-pnpm-patternsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
RidenDine uses a pnpm monorepo with 3 apps (web, admin, mobile) and 3 packages (shared, data, ui). Managing dependencies, ensuring type safety across packages, and running tasks efficiently requires proper workspace configuration and patterns.
Use this skill when:
ridendine-demo-main/
├── apps/
│ ├── web/ # Next.js 15 customer app
│ ├── admin/ # Next.js 15 admin dashboard
│ └── mobile/ # React Native/Expo 50 mobile app
├── packages/
│ ├── shared/ # Shared types, utilities
│ ├── data/ # Database client, repositories
│ └── ui/ # Shared UI components
├── backend/
│ └── supabase/ # Supabase migrations, Edge Functions
├── pnpm-workspace.yaml # Workspace configuration
├── package.json # Root package.json
└── turbo.json # Turbo configuration (if using Turbo)
Location: pnpm-workspace.yaml
Example Implementation:
packages:
- 'apps/*'
- 'packages/*'
Root package.json:
{
"name": "ridendine-monorepo",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "pnpm --parallel run dev",
"build": "pnpm --filter \"./apps/**\" run build",
"test": "pnpm --recursive run test",
"lint": "pnpm --recursive run lint",
"clean": "pnpm --recursive exec rm -rf node_modules .next dist out"
},
"devDependencies": {
"@types/node": "^20.10.0",
"typescript": "^5.3.3"
}
}
Using workspace:* Protocol:
Example: apps/web/package.json depends on packages/shared:
{
"name": "@ridendine/web",
"dependencies": {
"@home-chef/shared": "workspace:*",
"@home-chef/data": "workspace:*",
"@home-chef/ui": "workspace:*"
}
}
Why workspace:*:
Package Names:
// packages/shared/package.json
{
"name": "@home-chef/shared",
"version": "1.0.0",
"main": "./src/index.ts",
"types": "./src/index.ts"
}
// packages/data/package.json
{
"name": "@home-chef/data",
"version": "1.0.0",
"main": "./src/index.ts",
"types": "./src/index.ts"
}
// packages/ui/package.json
{
"name": "@home-chef/ui",
"version": "1.0.0",
"main": "./src/index.tsx",
"types": "./src/index.tsx"
}
Root tsconfig.json (base configuration):
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022"],
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"skipLibCheck": true,
"esModuleInterop": true,
"resolveJsonModule": true
}
}
App tsconfig.json (extends base):
// apps/web/tsconfig.json
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./*"],
"@home-chef/shared": ["../../packages/shared/src"],
"@home-chef/data": ["../../packages/data/src"],
"@home-chef/ui": ["../../packages/ui/src"]
},
"jsx": "preserve",
"lib": ["dom", "dom.iterable", "esnext"],
"plugins": [{ "name": "next" }]
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
Package tsconfig.json:
// packages/shared/tsconfig.json
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"baseUrl": ".",
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"declarationMap": true,
"composite": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
Install to root (applies to all packages):
pnpm add -D typescript @types/node -w
# -w flag installs to workspace root
Install to specific package:
# Add to web app
pnpm add --filter @ridendine/web next react react-dom
# Add to mobile app
pnpm add --filter @ridendine/mobile expo expo-router
# Add to shared package
pnpm add --filter @home-chef/shared zod
Add workspace dependency:
# Add shared package to web app
pnpm add --filter @ridendine/web @home-chef/shared@workspace:*
Update all dependencies:
pnpm update --recursive
Run script in specific package:
# Run dev server in web app
pnpm --filter @ridendine/web dev
# Run tests in shared package
pnpm --filter @home-chef/shared test
# Build admin app
pnpm --filter @ridendine/admin build
Run script in multiple packages:
# Run dev in all apps (parallel)
pnpm --filter "./apps/**" --parallel dev
# Build all packages first, then all apps
pnpm --filter "./packages/**" build
pnpm --filter "./apps/**" build
# Test everything
pnpm --recursive test
pnpm Filtering:
| Filter | Matches |
|--------|---------|
| --filter @ridendine/web | Specific package by name |
| --filter "./apps/**" | All packages in apps directory |
| --filter "./packages/**" | All packages in packages directory |
| --filter "!@ridendine/mobile" | All except mobile |
| --filter "...@home-chef/shared" | Shared package + all dependents |
| --recursive | All packages in workspace |
Example: packages/shared/src/index.ts
// Type exports
export type {
User,
Profile,
Chef,
Driver,
Order,
OrderItem,
Dish,
Review,
} from './types';
// Utility exports
export { formatCurrency, formatDate } from './utils/format';
export { validateEmail, validatePhone } from './utils/validation';
// Constant exports
export { ORDER_STATUSES, PAYMENT_STATUSES, USER_ROLES } from './constants';
Consuming in apps:
// apps/web/app/page.tsx
import type { Chef, Dish } from '@home-chef/shared';
import { formatCurrency } from '@home-chef/shared';
const price = formatCurrency(1599); // "$15.99"
Example: packages/data/src/repositories/orders.ts
import type { SupabaseClient } from '@supabase/supabase-js';
import type { Order, OrderItem } from '@home-chef/shared';
export class OrdersRepository {
constructor(private supabase: SupabaseClient) {}
async createOrder(data: {
customer_id: string;
chef_id: string;
total_cents: number;
items: Omit<OrderItem, 'id' | 'order_id'>[];
}): Promise<Order> {
const { data: order, error } = await this.supabase
.from('orders')
.insert({
customer_id: data.customer_id,
chef_id: data.chef_id,
total_cents: data.total_cents,
status: 'draft',
payment_status: 'pending',
})
.select()
.single();
if (error) throw error;
// Create order items
const { error: itemsError } = await this.supabase
.from('order_items')
.insert(
data.items.map((item) => ({
order_id: order.id,
...item,
}))
);
if (itemsError) throw itemsError;
return order;
}
async getOrdersByCustomer(customerId: string): Promise<Order[]> {
const { data, error } = await this.supabase
.from('orders')
.select('*, order_items(*)')
.eq('customer_id', customerId)
.order('created_at', { ascending: false });
if (error) throw error;
return data;
}
}
Export from package:
// packages/data/src/index.ts
export { OrdersRepository } from './repositories/orders';
export { ChefsRepository } from './repositories/chefs';
export { DriversRepository } from './repositories/drivers';
Use in app:
// apps/web/app/orders/actions.ts
'use server';
import { createClient } from '@/lib/supabase/server';
import { OrdersRepository } from '@home-chef/data';
export async function getMyOrders() {
const supabase = await createClient();
const {
data: { user },
} = await supabase.auth.getUser();
if (!user) throw new Error('Unauthorized');
const ordersRepo = new OrdersRepository(supabase);
return ordersRepo.getOrdersByCustomer(user.id);
}
Example: packages/ui/src/Button.tsx
import React from 'react';
export interface ButtonProps {
children: React.ReactNode;
variant?: 'primary' | 'secondary' | 'outline';
size?: 'sm' | 'md' | 'lg';
onClick?: () => void;
disabled?: boolean;
}
export function Button({
children,
variant = 'primary',
size = 'md',
onClick,
disabled = false,
}: ButtonProps) {
return (
<button
onClick={onClick}
disabled={disabled}
className={`btn btn-${variant} btn-${size}`}
>
{children}
</button>
);
}
Use in apps:
// apps/web/app/page.tsx
import { Button } from '@home-chef/ui';
export default function HomePage() {
return <Button variant="primary">Order Now</Button>;
}
Symptom: TypeScript or runtime error about missing module
Cause: Workspace dependency not installed or misconfigured
Fix:
pnpm install from rootpackage.json has workspace:* dependencypnpm-workspace.yaml includes package directorySymptom: TypeScript shows old types from shared package
Cause: Build cache or IDE cache
Fix:
pnpm --filter @home-chef/shared buildCmd+Shift+P → "TypeScript: Restart TS Server".next, dist folders: pnpm cleanpnpm install againSymptom: Import errors or infinite loops
Cause: Package A imports from B, B imports from A
Fix:
pnpm why <package>shared-utils)Symptom: Build fails because shared package not built first
Cause: No dependency ordering
Fix: Use topological sorting or Turbo:
Option 1: Manual ordering:
// Root package.json
{
"scripts": {
"build": "pnpm --filter \"./packages/**\" build && pnpm --filter \"./apps/**\" build"
}
}
Option 2: Turbo (recommended):
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "dist/**"]
},
"test": {
"dependsOn": ["build"]
}
}
}
Then: turbo run build (automatically builds dependencies first)
Build only affected packages:
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
with:
version: 8
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build affected packages
run: pnpm --filter "...[origin/main]" build
- name: Test affected packages
run: pnpm --filter "...[origin/main]" test
Filter explanation:
--filter "...[origin/main]" → Builds only packages changed since main branch1. Use pnpm's content-addressable store:
2. Enable hoisting carefully:
# .npmrc
hoist=false
hoist-pattern[]=*eslint*
hoist-pattern[]=*prettier*
3. Use Turbo for caching:
pnpm add -D turbo -w
# Turbo caches build outputs across runs
turbo run build --cache-dir=.turbo
4. Prune node_modules in CI:
pnpm install --frozen-lockfile --prod
# Removes devDependencies after build
pnpm-workspace.yaml, root package.jsondevelopment
Integrate Coinbase crypto payments into payment systems. Use when: (1) adding crypto payment support, (2) building onchain features, (3) implementing wallet functionality. Covers Coinbase Commerce (payment processor) vs CDP (developer platform), Server Wallets, Embedded Wallets, and multi-network support.
development
Add Apple Pay and Google Pay to Stripe checkout. Use when: (1) adding mobile wallet payments, (2) improving mobile conversion, (3) implementing one-tap checkout. Stripe Payment Request Button automatically detects device capabilities and shows Apple Pay (Safari/iOS) or Google Pay (Chrome/Android).
development
Master Vercel deployment for RidenDine web and admin Next.js apps. Use when: (1) deploying to production, (2) configuring environment variables, (3) setting up preview deployments, (4) debugging build failures, (5) configuring domains, (6) seeing "No Next.js version detected" error in Vercel builds, (7) setting up monorepo with separate projects on free tier. Key insight: Vercel monorepos require Root Directory configuration via dashboard (not vercel.json), GitHub integration auto-detects monorepo structure, free tier allows multiple projects.
development
Master Supabase Row Level Security (RLS) for RidenDine. Use when: (1) adding new tables, (2) modifying RLS policies, (3) debugging access control issues, (4) role-based data access. Key insight: All tables use RLS with role-based policies from profiles.role column.