mcpField
M
monorepo-setup

monorepo-setup

github.com/patricio0312rev開発63271 回インストール41 週間前に更新
リポジトリ

monorepo-setup について

Turborepo と pnpm workspaces で拡張性のあるモノレポを構築するための設定ガイドです。ワークスペース、共有パッケージ、ビルドパイプライン、CI/CD の基本構成を扱います。

主な機能

  1. 01pnpm ワークスペースの設定
  2. 02Turborepo ビルドパイプラインの構成
  3. 03共有 UI・ユーティリティパッケージの作成
  4. 04TypeScript と ESLint 設定の集約
  5. 05CI/CD とバージョン管理の計画

活用シーン

  1. 01JavaScript モノレポの新規構築
  2. 02複数アプリ間でのコンポーネントやユーティリティの共有
  3. 03チームのビルドと設定ルールの標準化

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/patricio0312rev/skills --skill monorepo-setup

インストール後、Claude Code で/monorepo-setupを入力して読み込まれたか確認します。

詳細

モノレポのセットアップ

Turborepo と pnpm ワークスペースを使用して、スケーラブルなモノレポを構成します。

コアワークフロー

  1. 構成の初期化:ワークスペースのレイアウトを作成します
  2. pnpm の構成:ワークスペースを設定します
  3. Turborepo の追加:ビルドパイプラインを構成します
  4. 共有パッケージの作成:共通ユーティリティを用意します
  5. アプリのセットアップ:パッケージを使用するアプリケーションを作成します
  6. CI/CD の構成:ビルドを最適化します

ディレクトリ構成

monorepo/
├── apps/
│   ├── web/                 # Next.js app
│   ├── api/                 # Express/Fastify API
│   └── mobile/              # React Native app
├── packages/
│   ├── ui/                  # Shared UI components
│   ├── config/              # Shared configs
│   ├── tsconfig/            # TypeScript configs
│   ├── eslint-config/       # ESLint configs
│   └── utils/               # Shared utilities
├── tooling/
│   ├── scripts/             # Build scripts
│   └── docker/              # Docker configs
├── turbo.json
├── pnpm-workspace.yaml
├── package.json
└── .npmrc

ルート構成

pnpm ワークスペース
# pnpm-workspace.yaml
packages:
  - "apps/*"
  - "packages/*"
  - "tooling/*"
ルートの package.json
{
  "name": "monorepo",
  "private": true,
  "packageManager": "[email protected]",
  "engines": {
    "node": ">=20.0.0",
    "pnpm": ">=8.0.0"
  },
  "scripts": {
    "build": "turbo run build",
    "dev": "turbo run dev",
    "lint": "turbo run lint",
    "test": "turbo run test",
    "typecheck": "turbo run typecheck",
    "clean": "turbo run clean && rm -rf node_modules",
    "format": "prettier --write \"**/*.{ts,tsx,md,json}\"",
    "changeset": "changeset",
    "version-packages": "changeset version",
    "release": "turbo run build --filter=./packages/* && changeset publish"
  },
  "devDependencies": {
    "@changesets/cli": "^2.27.0",
    "prettier": "^3.2.0",
    "turbo": "^2.0.0",
    "typescript": "^5.3.0"
  }
}
NPM の構成
# .npmrc
auto-install-peers=true
strict-peer-dependencies=false
shamefully-hoist=true
node-linker=hoisted

# Private registry (optional)
# @company:registry=https://npm.company.com/
# //npm.company.com/:_authToken=${NPM_TOKEN}

Turborepo の構成

// turbo.json
{
  "$schema": "https://turbo.build/schema.json",
  "globalDependencies": ["**/.env.*local"],
  "globalEnv": ["NODE_ENV", "CI"],
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**", "!.next/cache/**"],
      "env": ["DATABASE_URL", "API_URL"]
    },
    "dev": {
      "dependsOn": ["^build"],
      "cache": false,
      "persistent": true
    },
    "lint": {
      "dependsOn": ["^build"],
      "outputs": []
    },
    "typecheck": {
      "dependsOn": ["^build"],
      "outputs": []
    },
    "test": {
      "dependsOn": ["build"],
      "outputs": ["coverage/**"]
    },
    "clean": {
      "cache": false
    }
  }
}

共有 TypeScript 構成

// packages/tsconfig/base.json
{
  "$schema": "https://json.schemastore.org/tsconfig",
  "display": "Default",
  "compilerOptions": {
    "composite": false,
    "declaration": true,
    "declarationMap": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "inlineSources": false,
    "isolatedModules": true,
    "moduleResolution": "bundler",
    "noUnusedLocals": false,
    "noUnusedParameters": false,
    "preserveWatchOutput": true,
    "skipLibCheck": true,
    "strict": true,
    "strictNullChecks": true
  },
  "exclude": ["node_modules"]
}
// packages/tsconfig/nextjs.json
{
  "$schema": "https://json.schemastore.org/tsconfig",
  "display": "Next.js",
  "extends": "./base.json",
  "compilerOptions": {
    "lib": ["dom", "dom.iterable", "ES2022"],
    "module": "ESNext",
    "target": "ES2022",
    "jsx": "preserve",
    "noEmit": true,
    "plugins": [{ "name": "next" }],
    "allowJs": true,
    "incremental": true,
    "resolveJsonModule": true
  }
}
// packages/tsconfig/library.json
{
  "$schema": "https://json.schemastore.org/tsconfig",
  "display": "Library",
  "extends": "./base.json",
  "compilerOptions": {
    "lib": ["ES2022"],
    "module": "ESNext",
    "target": "ES2022",
    "outDir": "dist",
    "rootDir": "src"
  }
}

共有 UI パッケージ

// packages/ui/package.json
{
  "name": "@repo/ui",
  "version": "0.0.0",
  "private": true,
  "exports": {
    ".": "./src/index.ts",
    "./button": "./src/button.tsx",
    "./card": "./src/card.tsx",
    "./styles.css": "./src/styles.css"
  },
  "scripts": {
    "build": "tsup",
    "dev": "tsup --watch",
    "lint": "eslint src/",
    "typecheck": "tsc --noEmit",
    "clean": "rm -rf dist"
  },
  "peerDependencies": {
    "react": "^18.0.0",
    "react-dom": "^18.0.0"
  },
  "devDependencies": {
    "@repo/eslint-config": "workspace:*",
    "@repo/tsconfig": "workspace:*",
    "@types/react": "^18.2.0",
    "react": "^18.2.0",
    "tsup": "^8.0.0",
    "typescript": "^5.3.0"
  }
}
// packages/ui/src/button.tsx
import { ButtonHTMLAttributes, forwardRef } from 'react';

export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'primary' | 'secondary' | 'outline';
  size?: 'sm' | 'md' | 'lg';
}

export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
  ({ variant = 'primary', size = 'md', className = '', ...props }, ref) => {
    const baseStyles = 'inline-flex items-center justify-center rounded-md font-medium transition-colors';

    const variants = {
      primary: 'bg-blue-600 text-white hover:bg-blue-700',
      secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300',
      outline: 'border border-gray-300 bg-transparent hover:bg-gray-100',
    };

    const sizes = {
      sm: 'h-8 px-3 text-sm',
      md: 'h-10 px-4',
      lg: 'h-12 px-6 text-lg',
    };

    return (
      <button
        ref={ref}
        className={`${baseStyles} ${variants[variant]} ${sizes[size]} ${className}`}
        {...props}
      />
    );
  }
);

Button.displayName = 'Button';
// packages/ui/src/index.ts
export { Button, type ButtonProps } from './button';
export { Card, type CardProps } from './card';
// packages/ui/tsup.config.ts
import { defineConfig } from 'tsup';

export default defineConfig({
  entry: ['src/index.ts'],
  format: ['esm', 'cjs'],
  dts: true,
  splitting: false,
  sourcemap: true,
  clean: true,
  external: ['react', 'react-dom'],
});

共有ユーティリティパッケージ

// packages/utils/package.json
{
  "name": "@repo/utils",
  "version": "0.0.0",
  "private": true,
  "main": "./dist/index.js",
  "module": "./dist/index.mjs",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "import": "./dist/index.mjs",
      "require": "./dist/index.js",
      "types": "./dist/index.d.ts"
    }
  },
  "scripts": {
    "build": "tsup",
    "dev": "tsup --watch",
    "test": "vitest run",
    "test:watch": "vitest",
    "typecheck": "tsc --noEmit",
    "clean": "rm -rf dist"
  },
  "devDependencies": {
    "@repo/tsconfig": "workspace:*",
    "tsup": "^8.0.0",
    "typescript": "^5.3.0",
    "vitest": "^1.2.0"
  }
}
// packages/utils/src/index.ts
export { cn } from './cn';
export { formatDate, formatRelativeTime } from './date';
export { debounce, throttle } from './timing';
export { sleep, retry } from './async';
// packages/utils/src/cn.ts
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';

export function cn(...inputs: ClassValue[]): string {
  return twMerge(clsx(inputs));
}

Next.js アプリの構成

// apps/web/package.json
{
  "name": "@repo/web",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "typecheck": "tsc --noEmit",
    "clean": "rm -rf .next .turbo"
  },
  "dependencies": {
    "@repo/ui": "workspace:*",
    "@repo/utils": "workspace:*",
    "next": "^14.1.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "@repo/eslint-config": "workspace:*",
    "@repo/tsconfig": "workspace:*",
    "@types/node": "^20.11.0",
    "@types/react": "^18.2.0",
    "@types/react-dom": "^18.2.0",
    "typescript": "^5.3.0"
  }
}
// apps/web/tsconfig.json
{
  "extends": "@repo/tsconfig/nextjs.json",
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}
// apps/web/src/app/page.tsx
import { Button } from '@repo/ui/button';
import { formatDate } from '@repo/utils';

export default function Home() {
  return (
    <main className="flex min-h-screen flex-col items-center justify-center">
      <h1 className="text-4xl font-bold">Monorepo App</h1>
      <p className="mt-4">Today is {formatDate(new Date())}</p>
      <Button className="mt-4">Click me</Button>
    </main>
  );
}

ESLint 構成パッケージ

// packages/eslint-config/package.json
{
  "name": "@repo/eslint-config",
  "version": "0.0.0",
  "private": true,
  "exports": {
    "./base": "./base.js",
    "./next": "./next.js",
    "./react": "./react.js",
    "./library": "./library.js"
  },
  "devDependencies": {
    "@typescript-eslint/eslint-plugin": "^7.0.0",
    "@typescript-eslint/parser": "^7.0.0",
    "eslint": "^8.56.0",
    "eslint-config-next": "^14.1.0",
    "eslint-config-prettier": "^9.1.0",
    "eslint-plugin-react": "^7.33.0",
    "eslint-plugin-react-hooks": "^4.6.0"
  }
}
// packages/eslint-config/base.js
module.exports = {
  parser: '@typescript-eslint/parser',
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'prettier',
  ],
  plugins: ['@typescript-eslint'],
  env: {
    node: true,
    es2022: true,
  },
  rules: {
    '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
    '@typescript-eslint/no-explicit-any': 'warn',
  },
  ignorePatterns: ['dist/', 'node_modules/', '.turbo/'],
};
// packages/eslint-config/next.js
module.exports = {
  extends: [
    './base.js',
    'next/core-web-vitals',
    'plugin:react-hooks/recommended',
  ],
  rules: {
    'react/react-in-jsx-scope': 'off',
  },
};

CI/CD の構成

# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

env:
  TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
  TURBO_TEAM: ${{ vars.TURBO_TEAM }}

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 2

      - uses: pnpm/action-setup@v3
        with:
          version: 8

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Build
        run: pnpm build

      - name: Lint
        run: pnpm lint

      - name: Type check
        run: pnpm typecheck

      - name: Test
        run: pnpm test

Changeset の構成

// .changeset/config.json
{
  "$schema": "https://unpkg.com/@changesets/[email protected]/schema.json",
  "changelog": "@changesets/cli/changelog",
  "commit": false,
  "fixed": [],
  "linked": [],
  "access": "restricted",
  "baseBranch": "main",
  "updateInternalDependencies": "patch",
  "ignore": ["@repo/web", "@repo/api"]
}

ベストプラクティス

  1. ワークスペースプロトコル:内部依存関係には workspace:* を使用します
  2. 共有構成:TypeScript と ESLint の構成を一元管理します
  3. ビルドキャッシュ:Turborepo のリモートキャッシュを有効にします
  4. インクリメンタルビルド:dependsOn を正しく使用します
  5. パッケージのエクスポート:適切な exports フィールドを使用します
  6. バージョン管理:Changesets を使用します
  7. クリーン用スクリプト:各パッケージにクリーン用スクリプトを含めます
  8. 一貫した命名:@repo/ プレフィックスを使用します

出力チェックリスト

すべてのモノレポに含める項目:

  • pnpm-workspace.yaml の構成
  • 適切なパイプラインを設定した turbo.json
  • 共有 TypeScript 構成
  • 共有 ESLint 構成
  • UI コンポーネントライブラリ
  • ユーティリティパッケージ
  • 適切な package.json の exports
  • ライブラリのビルド用 tsup
  • キャッシュ機能付き CI/CD
  • バージョン管理用 Changeset

コメント