もはや pnpm と Turborepo で Monorepo 環境作れるから
この記事について
みなさん、こんにちは。
先日、pnpm + Turborepo + lerna-lite で作った Monorepo 環境の解説記事を書きました 👇
今回は簡易的な Monorepo 環境を作って上記の構成を解説して行こうかと思います 💪 ( 最低限の Monorepo 機能しかないので需要はあるかは分かりませんが... )
では、さっそくやっていきましょうー 🍫
ディレクトリ構造について
今回の作る Monorepo 環境の最終的な全体のディレクトリ構造は以下のようになります 👇
.
├── packages/
│ ├── lib-a/
│ | ├── src/
│ | | └──index.ts
│ │ └── package.json
│ └── lib-b/
│ ├── src/
│ | └──index.ts
│ └── package.json
├── package.json
├── pnpm-workspace.yaml
└── turbo.json
また、環境のバージョン関係は以下のようになります 👇
name | version |
---|---|
node.js | v18.5.0 |
pnpm | 7.21.0 |
上記が確認できましたら、最初はルートにある package.json の設定をしていきましょうー 🍏
package.json の設定
以下のコマンドを実行して package.json を作成します 👇
$> pnpm init
.
+ └── package.json
次に、生成された ./package.json
を以下のように編集します 👇
{
"name": "monorepo-example",
"version": "1.0.0",
"description": "",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "MIT",
+ "packageManager": "pnpm@7.19.0",
+ "engines": {
+ "pnpm": ">=7.19.0"
+ }
}
上記の "packageManager"
は Node.js v14.19.0 以上に標準搭載されている Corepack の設定です。この設定をして置くことで、他の人と同じバージョンのパッケージマネージャーを使用することができます。具体的な解説は以下の記事などを参考にすると良いと思います 👇
次に "engines"
を設定しておくと、想定していないバージョンで実行してしまった時にエラーを出すようにできます。今回は pnpm v7.19.0 以上を指定しています。
一応、以下のように "npm"
や "yarn"
などに "use pnpm please!"
のような値を設定しておくと、間違って yarn や npm を実行できないようにできます 👇
"engines": {
+ "npm": "use pnpm please!",
+ "yarn": "use pnpm please!",
"pnpm": ">=7.19.0"
}
しかし、配布するパッケージの package.json に書いてしまうとパッケージを使うユーザー側でエラーになってしまったり、npm の場合は engine-strict=true
設定が必要だったりと使い勝手が悪いので、今回は設定しないようにしています。
pnpm workspace の設定ファイルを作成する
次は workspace の設定をします。
pnpm の場合、./pnpm-workspace.yaml
にワークスペースに含むまたは除外するディレクトリーを glob パターンで指定するだけで設定できます 👇
packages:
- 'packages/*'
上記の設定で packages
直下のサブディレクトリが workspace の対象として扱えるようになりました!
次は実際に packages
にパッケージを実装していきましょうー 🎒
packages/lib-a を作成する
依存される側のパッケージを定義します。
まずはディレクトリと必要なパッケージをインストールします 👇
# lib-a のディレクトリを作成
$> mkdir -p ./packages/lib-a
$> cd ./packages/lib-a
# package.json を作成
$> pnpm init
# typescript をインストール
$> pnpm add -D typescript
次に、ソースコードを実装します。依存される側なので、簡単な変数のみ実装します 👇
export const message = 'HELLO WORLD!'
次に、package.json を以下のように記述します 👇
{
"name": "lib-a",
"version": "1.0.0",
+ "main": "./dist/index.js",
+ "scripts": {
+ "build": "tsc ./src/index.ts --outDir ./dist --declaration",
+ },
"devDependencies": {
"typescript": "^4.9.4"
}
}
ここまで記述できたら、以下のコマンドを実行してビルドファイルが正しく出力されていれば OK👌 です。
$> cd ./packages/lib-a
$> pnpm build
> lib-a@1.0.0 build /monorepo-example/packages/lib-a
> tsc ./src/index.ts --outDir ./dist --declaration
# ルートに居る状態で以下のコマンド実行しても同じように出来ます
# $> pnpm --filter lib-a build
これで lib-a パッケージの実装は完了です。
次は lib-a に依存するパッケージである lib-b を実装していきましょうー 🥬
packages/lib-b を作成する
まずは lib-b のディレクトリと package.json を作成します 👇
# lib-b のディレクトリを作成
$> mkdir -p ./packages/lib-b
$> cd ./packages/lib-b
# package.json を作成
$> pnpm init
# typescript をインストール
$> pnpm add -D typescript
次に、lib-b は lib-a に依存するので以下のコマンドで lib-a をインストールします 👇
$> cd ./packages/lib-b
$> pnpm add lib-a
# ルートに居る状態で以下のコマンド実行しても同じように出来ます
# $> pnpm --filter lib-b add lib-a
上記のコマンドが成功したら、次は package.json を以下のように修正します 👇
{
"name": "lib-b",
"version": "1.0.0",
+ "main": "./dist/index.js",
+ "scripts": {
+ "build": "tsc ./src/index.ts --outDir ./dist --declaration",
+ },
"devDependencies": {
"typescript": "^4.9.4"
},
"dependencies": {
- "lib-a": "workspace:^1.0.0"
+ "lib-a": "workspace:*"
}
}
"main"
や "build"
は lib-a の時と同じですが、"dependencies"
内の lib-a のバージョンを "workspace:*"
に修正しています。これによって、常にローカルの lib-a の方を参照するようになります。( ※ もしこの挙動が嫌な場合は、適切なバージョンを設定してください。 )
次に src/index.ts
を以下のように実装します 👇
import { messaage } from "lib-a";
console.log(`${messaage} from lib-b`)
ここまで実装できたら、build タスクを実行して正しくビルドできたら OK👌 です。
$> cd ./packages/lib-b
$> pnpm build
> lib-a@1.0.0 build /monorepo-example/packages/lib-a
> tsc ./src/index.ts --outDir ./dist --declaration
# ルートに居る状態で以下のコマンド実行しても同じように出来ます
# $> pnpm --filter lib-b build
ビルドが成功したら、ビルドファイルを実行してみてテキストが正しく表示されていれば lib-b の完成です!
$> node ./dist/index.js
HELLO WORLD! from lib-b
ここまで出来れば、workspace の設定&作成は完了です。
次はより workspace 内のタスクを扱いやすくするための設定を行っていきます 🎯
Turborepo でタスクを実行する
Turborepo は Monorepo 内のタスクを依存関係を考慮して実行してくれるツールです。
今回は ./packages
内の workspace の build
コマンドを一括で実行できるようにしたいと思います。
まずは、Turborepo をルートにインストールします 👇
# ルートにインストールする場合は`-w, --workspace-root`が必要です
$> pnpm add -w -D turbo
インストールが完了したら、次は ./turbo.json
を以下のように記述します 👇
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
}
}
"dependsOn"
に "^build"
を設定することで、依存関係を考慮して build
タスクを実行してくれます。( 今回の場合は lib-a の build
→ lib-b の build
の順番で実行してくれます )
"outputs"
にビルド結果を含むディレクトリやファイルのパスを glob パターンで設定すると、ファイルの差分を考慮してタスクを実行してくれます。これにより、変更の無いパッケージを無駄にビルドしなくて済むようになります。
これで Turborepo の設定は完了です。
実行しやすいように package.json の "scripts"
に "build"
を追加します 👇
"scripts": {
+ "build": "turbo build",
"test": "echo \"Error: no test specified\" && exit 1"
},
追加できたら以下のコマンドを実行して正しくビルドできれば OK👌 です。
$> pnpm build
> monorepo-example@1.0.0 build /monorepo-example
> turbo build
• Packages in scope: lib-a, lib-b
• Running build in 2 packages
• Remote caching disabled
lib-a:build: cache miss, executing 5107e54a8a529999
lib-a:build:
lib-a:build: > lib-a@1.0.0 build /monorepo-example/packages/lib-a
lib-a:build: > tsc ./src/index.ts --outDir ./dist --declaration
lib-a:build:
lib-b:build: cache miss, executing 87efad3afd8b858c
lib-b:build:
lib-b:build: > lib-b@1.0.0 build /monorepo-example/packages/lib-b
lib-b:build: > tsc ./src/index.ts --outDir ./dist --declaration
lib-b:build:
Tasks: 2 successful, 2 total
Cached: 0 cached, 2 total
Time: 3.942s
完成 ✨
はい、これで今回の Monorepo 環境構築は完了です。お疲れさまでした!
基本的には、pnpm workspace と Turobrepo の設定だけなので、そこまで難しくは無かったと思いますが、扱うパッケージによってはより複雑な設定が必要になるかもしれません。その際は、Turborepo の便利なオプションが色々ありますので、確認しておくと良いかと思います 👇
また、pnpm の --filter
オプションを活用すると、パッケージのコマンドをルートから実行できたりして開発が捗ると思いますので、こちらも確認しておくと良いかと思います 👇
リリースフローについて
リリースフローについては、プロジェクトによって変わってきますので、ここでは解説しませんが、一例として zenn-editor では Lerna-Lite を使用しています 👇
また、柔軟なリリースフローを構築できるツールとして Changesets などがありますので、そちらも検討してみてもいいかもしれません 👇
あとがき
ここまで読んでくれてありがとうございます 🙏
Monorepo の環境構築というと Lerna などのようなツールを使うことが多いと思いますが、今ではパッケージマネージャーに備わっている workspace 機能と、それを補助するツールを使うだけでもある程度の Monorepo 環境は作れると思いますので、この機会に是非やってみてはいかがでしょうか。
私は zenn-editor で今回の構成をやってみたので、己の Monorepo 力を磨きながら運用していこうかと思います。( もし負債になったら責任もって私が返済する覚悟 😇 )
ここまで読んでいただいてありがとうございます 🙏
記事に間違いなどがあれば、コメントなどで教えて頂けると嬉しいです。
これが誰かの参考になれば幸いです。
それではまた 👋
Discussion
prismaなどの複数パッケージ間で同じバージョンにしたいといったバージョン管理については、今の所手動で調節するしかないですかね?
Monorepo ではルートに共通のパッケージをインストールすることで複数のパッケージ間で同じバージョンのパッケージを使用することができます。
具体例を挙げると、この記事の場合は
typescript@^4.9.4
がlib-a
,lib-b
それぞれでインストールされていますが、これをルートの node_modules にまとめることができます 👇このようにまとめた場合、ルートの
typescript
を参照するように ( 巻き上げが発生しないように )lib-a
,lib-b
それぞれにインストールしていたtypescript
を削除する必要があります 👇ここまでを実行すると、package.json の依存関係は以下のようになります 👇
これで、
lib-a
,lib-b
で同じバージョンのtypescript
を使用することができます。( もちろん、これまで通りlib-a
,lib-b
内の npm scripts でもtsc
コマンドを使用できます )具体例までありがとうございます
よく理解できました!