T-CREATOR

Cline × Monorepo(Yarn Workspaces)導入:パス解決とルート権限の最適解

Cline × Monorepo(Yarn Workspaces)導入:パス解決とルート権限の最適解

Monorepo 構成で複数のパッケージを管理している開発現場において、AI コーディングアシスタント「Cline」を導入する際、パス解決やルート権限の問題に直面することがあります。 特に Yarn Workspaces を採用したプロジェクトでは、各パッケージ間の依存関係や、ルートディレクトリと個別パッケージディレクトリの扱いが複雑になりがちです。

本記事では、Cline を Monorepo 環境で効果的に活用するための設定方法、パス解決の最適化、そしてルート権限の適切な管理方法について、実例を交えながら解説していきます。

背景

Monorepo と Yarn Workspaces の基本構造

Monorepo は、複数のプロジェクトやパッケージを単一のリポジトリで管理する開発手法です。 Yarn Workspaces を利用することで、各パッケージ間の依存関係を効率的に管理し、共通のモジュールを重複なく共有できます。

典型的な Monorepo 構造は以下のようになります。

typescriptproject-root/
├── package.json          // ルートパッケージ定義
├── yarn.lock            // 依存関係ロック
├── packages/
│   ├── web/            // フロントエンドアプリ
│   │   ├── package.json
│   │   └── src/
│   ├── api/            // バックエンド API
│   │   ├── package.json
│   │   └── src/
│   └── shared/         // 共有ライブラリ
│       ├── package.json
│       └── src/
└── node_modules/       // 共有依存関係

下記の図で、Monorepo における各パッケージの関係性を視覚化してみましょう。

mermaidflowchart TB
  root["プロジェクトルート<br/>(Workspaces 管理)"]
  web["packages/web<br/>(フロントエンド)"]
  api["packages/api<br/>(バックエンド)"]
  shared["packages/shared<br/>(共有ライブラリ)"]
  nodeModules[("node_modules<br/>(共有依存関係)")]

  root -->|管理| web
  root -->|管理| api
  root -->|管理| shared
  root -->|集約| nodeModules

  web -.->|参照| shared
  api -.->|参照| shared
  web -.->|利用| nodeModules
  api -.->|利用| nodeModules
  shared -.->|利用| nodeModules

図で理解できる要点:

  • ルートディレクトリが各パッケージを統括管理している
  • 共有ライブラリ(shared)を複数パッケージから参照できる
  • node_modules はルートで集約され、全パッケージで共有される

Cline の役割と特徴

Cline は、VSCode 拡張機能として動作する AI コーディングアシスタントで、コード生成、リファクタリング、バグ修正などを支援してくれます。 Claude 3.5 Sonnet などの大規模言語モデルを活用し、自然言語での指示から実際のコード実装まで、開発フローを大幅に効率化できるのが特徴ですね。

ただし、Cline はデフォルトで VSCode のワークスペースルートを基準に動作するため、Monorepo 環境では以下のような課題が生じます。

課題

パス解決の複雑性

Monorepo 環境では、Cline がファイルパスを解決する際に、以下のような問題が発生しやすくなります。

相対パスの基準点が不明確になることで、Cline が生成するコードのインポート文が不正確になったり、ファイル参照が失敗したりすることがあります。 各パッケージには独自の tsconfig.jsonpackage.json が存在するため、パスエイリアスの解決方法も異なるのです。

具体的な問題例を見てみましょう。

typescript// packages/web/src/components/UserList.tsx で発生する問題

// ❌ Cline が誤って生成するパス(ルート基準)
import { User } from '../../../shared/src/types/User';

// ⭕ 正しいパス(Workspace エイリアス使用)
import { User } from '@myapp/shared/types/User';

上記のように、Cline がルートディレクトリを基準にした相対パスを生成してしまうと、実際のビルド時にパスが解決できません。 Yarn Workspaces のパッケージ名(@myapp​/​shared)を使ったインポートが推奨されるのですが、Cline はこの規約を自動的に理解できないのです。

ルート権限とワークスペース権限の競合

Monorepo において Cline を使用する際、どのディレクトリをワークスペースルートとして開くかという選択が重要になります。

以下の 3 つの選択肢があり、それぞれにメリットとデメリットがあります。

#開き方Cline の動作範囲メリットデメリット
1プロジェクトルート全パッケージアクセス可能全体を俯瞰できる不要なファイルまで認識
2個別パッケージ該当パッケージのみパス解決が単純化他パッケージ参照不可
3マルチルートワークスペース指定したパッケージのみ柔軟な範囲設定設定が複雑化

下記のフローチャートで、ワークスペースの選択による影響を整理してみましょう。

mermaidflowchart TD
  start["Cline 起動"]
  choice["ワークスペース選択"]
  rootWs["ルートを開く"]
  pkgWs["個別パッケージを開く"]
  multiWs["マルチルート設定"]

  rootIssue["課題:スコープが広すぎる<br/>不要ファイルまで認識"]
  pkgIssue["課題:他パッケージ参照不可<br/>共有ライブラリにアクセスできない"]
  multiSolution["解決:必要な範囲のみ認識<br/>適切な権限管理"]

  start --> choice
  choice --> rootWs
  choice --> pkgWs
  choice --> multiWs

  rootWs --> rootIssue
  pkgWs --> pkgIssue
  multiWs --> multiSolution

  rootIssue -.->|改善策| multiSolution
  pkgIssue -.->|改善策| multiSolution

図で理解できる要点:

  • ルートを開くと範囲が広すぎて効率が低下
  • 個別パッケージだと共有ライブラリへのアクセスが制限される
  • マルチルートワークスペースが最適なバランスを提供

プロジェクトルート全体を開いた場合、Cline は node_modules や各パッケージの dist.next などのビルド成果物まで認識範囲に含めてしまい、パフォーマンスが低下しますね。 一方、個別パッケージのみを開くと、共有ライブラリ(packages​/​shared)への参照が困難になるのです。

設定ファイルの優先順位問題

Monorepo では、ルートと各パッケージに複数の設定ファイルが存在します。

typescript// ルートの tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@myapp/*": ["packages/*/src"]
    }
  }
}
typescript// packages/web/tsconfig.json
{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "baseUrl": "./src",
    "paths": {
      "@/*": ["./*"]
    }
  }
}

Cline がどちらの設定を優先するかが不明確だと、生成されるコードのインポートパスが一貫性を欠いてしまいます。 特に、baseUrlpaths の設定が競合すると、開発環境では動作するがビルド時にエラーになるという問題が発生するでしょう。

解決策

マルチルートワークスペースの活用

最も効果的な解決策は、VSCode の マルチルートワークスペース機能 を活用することです。 これにより、Cline が認識すべきディレクトリを明示的に指定し、適切なスコープで動作させられます。

ワークスペース設定ファイルの作成

まず、プロジェクトルートに .code-workspace ファイルを作成しましょう。 このファイルで、Cline に認識させたいディレクトリを指定します。

json{
  "folders": [
    {
      "name": "Root Config",
      "path": "."
    },
    {
      "name": "Web App",
      "path": "packages/web"
    },
    {
      "name": "Shared Library",
      "path": "packages/shared"
    }
  ]
}

上記の設定では、ルートの設定ファイル(package.jsontsconfig.json など)と、実際に開発する web パッケージ、そして共有ライブラリである shared を指定しています。 api パッケージなど、現在作業しないディレクトリは除外することで、Cline の認識範囲を最適化できるのです。

ワークスペースの開き方

作成した .code-workspace ファイルを VSCode で開くには、以下のコマンドを実行します。

bashcode myapp.code-workspace

または、VSCode のメニューから「ファイル」→「ワークスペースを開く」を選択し、作成した .code-workspace ファイルを選択してください。 これにより、指定したディレクトリのみが VSCode のエクスプローラーに表示され、Cline もこの範囲内でのみ動作するようになります。

Cline 設定ファイルによるパス解決の最適化

Cline には、プロジェクト固有の設定を記述できる cline_docs ディレクトリがあります。 ここに、Monorepo 特有のパス解決ルールを明示的に記載しましょう。

プロジェクトルールの定義

プロジェクトルートに .clinerules ファイルを作成します。 このファイルで、Cline に対してパス解決のルールを指示できます。

markdown# Monorepo パス解決ルール

# インポートパスの原則

1. 同一パッケージ内のファイルは相対パスを使用
2. 他パッケージのファイルは Workspace エイリアスを使用
3. 共有ライブラリは必ず `@myapp/shared` から始めるパスを使用

# 例

## 同一パッケージ内

// packages/web/src/components/UserList.tsx から
import { Button } from './Button';
import { useAuth } from '../hooks/useAuth';

## 他パッケージ参照

// packages/web/src から packages/shared を参照
import { User } from '@myapp/shared/types/User';
import { formatDate } from '@myapp/shared/utils/date';

上記のルールファイルを作成することで、Cline はコード生成時にこのルールに従ったインポート文を生成するようになります。 特に、Workspace エイリアス(@myapp​/​shared)の使用を明示することが重要ですね。

TypeScript パス設定との連携

Cline が TypeScript のパス設定を正しく理解できるよう、ルートの tsconfig.json を適切に設定しましょう。

json{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@myapp/shared/*": ["packages/shared/src/*"],
      "@myapp/web/*": ["packages/web/src/*"],
      "@myapp/api/*": ["packages/api/src/*"]
    }
  }
}

各パッケージの tsconfig.json では、ルートの設定を継承しつつ、パッケージ固有の設定を追加します。

json{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "baseUrl": "./src",
    "paths": {
      "@/*": ["./*"]
    }
  },
  "include": ["src/**/*"]
}

この設定により、パッケージ内では @​/​components​/​Button のような短縮パスが使用でき、他パッケージからは @myapp​/​web​/​components​/​Button でアクセスできるようになります。 Cline はこの両方の形式を理解し、適切な方を選択してくれるでしょう。

ルート権限の明確化と制限

Cline に対して、どのディレクトリで何ができるかを明確に指示することで、意図しない変更を防止できます。

.gitignore との連携

まず、Cline が認識すべきでないディレクトリを .gitignore で明示しましょう。 Cline はデフォルトで .gitignore に記載されたディレクトリを無視します。

bash# ビルド成果物
**/dist
**/build
**/.next
**/out

# 依存関係
**/node_modules

# キャッシュ
**/.cache
**/.turbo

# ログファイル
**/*.log
**/logs

この設定により、Cline はビルド成果物やキャッシュファイルを認識範囲から除外し、ソースコードのみに集中できます。 特に、node_modules を除外することで、パフォーマンスが大幅に向上するでしょう。

VSCode 設定による制限

さらに、VSCode の設定ファイル(.vscode​/​settings.json)で、Cline が監視すべきファイルパターンを制限できます。

json{
  "files.watcherExclude": {
    "**/node_modules/**": true,
    "**/dist/**": true,
    "**/.next/**": true,
    "**/build/**": true
  },
  "search.exclude": {
    "**/node_modules": true,
    "**/dist": true,
    "**/.next": true,
    "**/build": true,
    "**/yarn.lock": true
  }
}

この設定により、ファイルウォッチャーや検索機能から不要なディレクトリが除外され、Cline のレスポンス速度が向上します。 大規模な Monorepo では、この最適化が特に効果的ですね。

パッケージ間依存関係の可視化

Cline が各パッケージ間の依存関係を理解できるよう、ドキュメントとして明示しましょう。

mermaidflowchart LR
  web["@myapp/web<br/>(Next.js アプリ)"]
  api["@myapp/api<br/>(Express API)"]
  shared["@myapp/shared<br/>(共通ライブラリ)"]

  web -->|依存| shared
  api -->|依存| shared

  web -.->|API 呼び出し| api

図で理解できる要点:

  • web と api は両方とも shared に依存している
  • web は api を直接インポートせず、API 呼び出しでやり取りする
  • shared は他パッケージに依存しない独立したライブラリ

プロジェクトルートに docs​/​architecture.md のようなファイルを作成し、この依存関係を記載しておくと良いでしょう。

markdown# パッケージ依存関係

# 依存ルール

- `@myapp/shared` は他のパッケージに依存しない
- `@myapp/web``@myapp/api``@myapp/shared` にのみ依存する
- パッケージ間の直接インポートは禁止

# 共有ライブラリの役割

`@myapp/shared` には以下を配置:

- 型定義(User, Product など)
- ユーティリティ関数(日付フォーマット、バリデーションなど)
- 定数(API エンドポイント、設定値など)

Cline はこのドキュメントを参照し、適切なインポート文を生成できるようになります。

具体例

実際の開発フローに沿って、Cline を Monorepo 環境で使用する具体例を見ていきましょう。

新規コンポーネントの作成

packages​/​web に新しいユーザー一覧コンポーネントを作成する場合を考えてみます。

ステップ 1:ワークスペースの準備

マルチルートワークスペースを開いた状態で、Cline に以下のように指示します。

swiftpackages/web/src/components に UserList.tsx を作成してください
@myapp/shared/types/User 型を使用しユーザー一覧を表示するコンポーネントを実装してください

Cline は、.clinerules で定義したルールに従い、適切なインポート文を持つコンポーネントを生成します。

ステップ 2:生成されたコード

Cline が生成するコードは以下のようになります。

typescript// packages/web/src/components/UserList.tsx

// 他パッケージからの型インポート(Workspace エイリアス使用)
import { User } from '@myapp/shared/types/User';

// 同一パッケージ内のコンポーネント(相対パス使用)
import { UserCard } from './UserCard';

上記のように、他パッケージからのインポートは Workspace エイリアスが使用され、同一パッケージ内のインポートは相対パスが使用されています。 これにより、TypeScript のコンパイラが正しくパスを解決できるのです。

ステップ 3:コンポーネントの実装

コンポーネント本体の実装部分を見てみましょう。

typescript// Props の型定義
interface UserListProps {
  users: User[];
  onUserClick?: (user: User) => void;
}

// コンポーネント実装
export const UserList: React.FC<UserListProps> = ({
  users,
  onUserClick,
}) => {
  return (
    <div className='user-list'>
      {users.map((user) => (
        <UserCard
          key={user.id}
          user={user}
          onClick={() => onUserClick?.(user)}
        />
      ))}
    </div>
  );
};

Cline は、@myapp​/​shared​/​types​/​User から取得した型定義を使用し、型安全なコンポーネントを生成してくれます。 UserCard コンポーネントは同一パッケージ内にあるため、相対パスでインポートされていますね。

共有ユーティリティ関数の追加

次に、複数のパッケージから使用される共有ユーティリティ関数を packages​/​shared に追加する例を見てみましょう。

ステップ 1:関数の配置場所を指定

bashpackages/shared/src/utils に formatUserName.ts を作成してください。
User 型を受け取り、姓名を適切にフォーマットして返す関数を実装してください。

ステップ 2:型のインポート

Cline が生成するコードでは、同じパッケージ内の型定義も適切にインポートされます。

typescript// packages/shared/src/utils/formatUserName.ts

// 同一パッケージ内の型インポート(相対パス使用)
import { User } from '../types/User';

packages​/​shared 内では、すべて相対パスでインポートが行われます。 これは、このパッケージが他から参照される際に Workspace エイリアスで呼び出されるためですね。

ステップ 3:関数の実装

typescript/**
 * ユーザーの姓名をフォーマットする
 * @param user - ユーザーオブジェクト
 * @param format - フォーマット形式('full' | 'last-first' | 'first-only')
 * @returns フォーマットされた名前
 */
export const formatUserName = (
  user: User,
  format: 'full' | 'last-first' | 'first-only' = 'full'
): string => {
  const { firstName, lastName } = user;

  switch (format) {
    case 'full':
      return `${firstName} ${lastName}`;
    case 'last-first':
      return `${lastName} ${firstName}`;
    case 'first-only':
      return firstName;
    default:
      return `${firstName} ${lastName}`;
  }
};

この関数は packages​/​shared に配置されているため、packages​/​webpackages​/​api から以下のようにインポートできます。

typescript// packages/web/src/components/UserProfile.tsx から使用
import { formatUserName } from '@myapp/shared/utils/formatUserName';
import { User } from '@myapp/shared/types/User';

const displayName = formatUserName(user, 'full');

下記のシーケンス図で、コンポーネントから共有ユーティリティを呼び出す流れを示します。

mermaidsequenceDiagram
  participant Component as UserProfile<br/>(web パッケージ)
  participant Util as formatUserName<br/>(shared パッケージ)
  participant Type as User 型<br/>(shared パッケージ)

  Component->>Type: User 型をインポート
  Component->>Util: formatUserName をインポート
  Component->>Util: formatUserName(user, 'full')
  Util->>Type: User 型で検証
  Util-->>Component: "山田 太郎" を返却
  Component->>Component: 画面に表示

図で理解できる要点:

  • コンポーネントは shared パッケージから型と関数をインポート
  • ユーティリティ関数内で型安全性が保証される
  • パッケージ境界を超えた型チェックが機能する

API との連携実装

最後に、packages​/​web から packages​/​api のエンドポイントを呼び出す実装例を見てみましょう。

ステップ 1:API 定数の定義

まず、packages​/​shared に API エンドポイントの定数を定義します。

typescript// packages/shared/src/constants/apiEndpoints.ts

export const API_ENDPOINTS = {
  USERS: {
    LIST: '/api/users',
    DETAIL: (id: string) => `/api/users/${id}`,
    CREATE: '/api/users',
    UPDATE: (id: string) => `/api/users/${id}`,
    DELETE: (id: string) => `/api/users/${id}`,
  },
} as const;

この定数を shared パッケージに配置することで、web と api の両方から参照でき、エンドポイントの変更が容易になります。

ステップ 2:API クライアントの実装

packages​/​web に API クライアント関数を作成します。

typescript// packages/web/src/lib/api/userApi.ts

// 共有パッケージからのインポート
import { User } from '@myapp/shared/types/User';
import { API_ENDPOINTS } from '@myapp/shared/constants/apiEndpoints';
typescript/**
 * ユーザー一覧を取得する
 * @returns ユーザーの配列
 */
export const fetchUsers = async (): Promise<User[]> => {
  const response = await fetch(API_ENDPOINTS.USERS.LIST);

  if (!response.ok) {
    throw new Error(
      `Failed to fetch users: ${response.statusText}`
    );
  }

  return response.json();
};
typescript/**
 * ユーザー詳細を取得する
 * @param id - ユーザー ID
 * @returns ユーザーオブジェクト
 */
export const fetchUserById = async (
  id: string
): Promise<User> => {
  const response = await fetch(
    API_ENDPOINTS.USERS.DETAIL(id)
  );

  if (!response.ok) {
    throw new Error(
      `Failed to fetch user: ${response.statusText}`
    );
  }

  return response.json();
};

Cline は、@myapp​/​shared から型定義と定数をインポートし、型安全な API クライアント関数を生成してくれます。 エラーハンドリングも適切に実装されていますね。

ステップ 3:コンポーネントでの使用

生成された API クライアントを、コンポーネントから使用します。

typescript// packages/web/src/components/UserListContainer.tsx

import { useEffect, useState } from 'react';
import { User } from '@myapp/shared/types/User';
import { fetchUsers } from '../lib/api/userApi';
import { UserList } from './UserList';
typescriptexport const UserListContainer: React.FC = () => {
  const [users, setUsers] = useState<User[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const loadUsers = async () => {
      try {
        setLoading(true);
        const data = await fetchUsers();
        setUsers(data);
      } catch (err) {
        setError(
          err instanceof Error
            ? err.message
            : 'Unknown error'
        );
      } finally {
        setLoading(false);
      }
    };

    loadUsers();
  }, []);

  if (loading) return <div>読み込み中...</div>;
  if (error) return <div>エラー: {error}</div>;

  return <UserList users={users} />;
};

このように、Cline は Monorepo の構造を理解し、適切なインポートパスを使用したコードを生成してくれるのです。

トラブルシューティング例

実際の開発では、パス解決に関するエラーが発生することがあります。 代表的なエラーと解決方法を見てみましょう。

エラー 1:モジュールが見つからない

bashError: Cannot find module '@myapp/shared/types/User'

発生条件:

  • TypeScript の paths 設定が正しくない
  • Yarn Workspaces の設定が反映されていない

解決方法:

  1. ルートの package.json で Workspaces が正しく定義されているか確認します
json{
  "name": "myapp",
  "private": true,
  "workspaces": ["packages/*"]
}
  1. 各パッケージの package.json に正しい名前が設定されているか確認します
json{
  "name": "@myapp/shared",
  "version": "1.0.0",
  "main": "src/index.ts"
}
  1. yarn install を実行して、Workspaces のリンクを再構築します
bashyarn install
  1. VSCode の TypeScript サーバーを再起動します(Cmd + Shift + P → "TypeScript: Restart TS Server")

これらの手順により、モジュール解決エラーが解消されるでしょう。

エラー 2:型定義が重複する

bashError: Duplicate identifier 'User'

発生条件:

  • 同じ型が複数の場所で定義されている
  • Cline が誤って型を再定義してしまった

解決方法:

  1. プロジェクト内で型名を検索します
bashyarn grep -r "interface User" packages/
  1. 重複している型定義を削除し、@myapp​/​shared の型のみを使用するよう修正します
typescript// ❌ 削除:各パッケージでの型定義
// packages/web/src/types/User.ts
export interface User { ... }

// ⭕ 使用:共有パッケージの型
import { User } from '@myapp/shared/types/User';
  1. .clinerules ファイルに型の定義場所を明記します
markdown# 型定義のルール

すべての共通型は `@myapp/shared/types` に定義し、他のパッケージからインポートする。
各パッケージで独自の型を定義しない。

このルールを明記することで、Cline が誤って型を再定義することを防げます。

まとめ

Cline を Monorepo 環境で効果的に活用するには、以下の 3 つのポイントが重要です。

マルチルートワークスペースの活用により、Cline が認識すべき範囲を適切に制限し、パフォーマンスと精度を向上させられます。 .code-workspace ファイルで必要なパッケージのみを指定することで、不要なディレクトリを除外できるのです。

明示的なパス解決ルールの定義も欠かせません。 .clinerules ファイルで Workspace エイリアスの使用方法を明記し、TypeScript の paths 設定と連携させることで、Cline が一貫性のあるインポート文を生成してくれます。

適切なルート権限の管理により、意図しない変更を防止できるでしょう。 .gitignore や VSCode の設定ファイルで、Cline がアクセスすべきでないディレクトリを明確に除外することが大切です。

これらの設定を適切に行うことで、Cline は Monorepo 環境でも高い生産性を発揮し、開発者の強力なパートナーとなってくれます。 Yarn Workspaces の利点を最大限に活かしつつ、AI アシスタントの恩恵を受けられる環境を構築できるのですね。

初期設定には多少の手間がかかりますが、一度整備すれば、チーム全体の開発効率が大幅に向上するでしょう。 ぜひ、本記事で紹介した設定方法を参考に、あなたのプロジェクトでも Cline × Monorepo の組み合わせを試してみてください。

関連リンク