
2026/01/20 18:33
**パッケージマネージャにおけるワークスペースとモノレポ**
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
要約
ワークスペースは、開発中にローカルパッケージを相互にリンクできるため、多くのパッケージマネージャで普及した機能です。これにより、変更を公開したりシンボリックリンクを作成する必要がなくなり、依存関係の更新が関連プロジェクト全体で即座に反映され、調整コストが削減されます。
この記事では各エコシステムがワークスペースをどのように実装しているかを説明しています:
- npm(v7以降) – ルート
にpackage.json
を宣言し、シンボリックリンクを作成します。依存関係はルートへホイストされますが、ワークスペース対応の公開機能はありません。"workspaces": ["packages/*"] - pnpm – 依存関係をホイストせず、コンテンツアドレス可能なストアを使用し、
プロトコルでローカル解決を強制します。公開時には実際のバージョンに置き換えられます。"workspace:*" - Cargo – メンバー間で単一の
を共有し、一つのターゲットディレクトリへビルド、機能はグローバルに解決します。ワークスペースメンバーを依存関係順に公開できます(Cargo.lock
)。cargo publish - Go(1.18以降) –
ファイルでローカルで解決するモジュールを列挙します。通常はバージョン管理から除外され、公開モジュールの一部ではなくローカル開発の便宜として機能します。go.work - その他のエコシステム(Bundler, Composer, SwiftPM, Dart pub, Elixir Mix, NuGet)ではパスベースのローカル依存関係が提供されますが、正式なワークスペースサポートや公開認識はなく、リリース前に手動で調整する必要があります。
ワークスペースは、JavaScriptマネージャ(Yarn, npm, pnpm, Bun)、Rust の Cargo、Go 1.18+、およびその他のエコシステムによって独立して採用されました。これはローカル変更を公開し複数パッケージを調整するコストが高いためです。
ワークスペースに共通する問題としては、ホイストによる仮想依存関係、開発中に無視されるバージョン不一致、ローカルと新規インストール間のCI差分、ビルドオーケストレーションのギャップ、および組み込み公開調整機能の欠如があります。
Turborepo, Nx, Changesets, Lerna, および Cargo の publish コマンドなどのツールは、ワークスペースパッケージ間でビルド順序、キャッシュ、およびバージョンアップを調整しますが、コアワークスペース機能セットとは別物です。
著者は自身がワークスペースを必要とした経験がないことを指摘し、この機能は調整摩擦を減らすために生まれたものであると述べています。読者には、ワークスペースを使用する動機やメリットが追加の複雑さを上回るかどうかについて経験を共有してほしいと呼びかけています。
本文
私はワークスペースを使ったことがありません。モノレポも使ったことがないですし、巨大なチームで働いた経験もありません。
私が手掛けるプロジェクトは小規模なので、リポジトリごとに1つのパッケージで十分ですし、複数パッケージ間で変更を調整する必要がある場合でも、公開作業はそれほど面倒ではありません。
ところが、現在主流のパッケージマネージャーならほぼ必ずワークスペース(または同等の仕組み)を持っています。JavaScript で言えば Yarn・npm・pnpm・Bun。Rust の Cargo、Python の uv、Go の go.work、PHP の Composer、Dart の pub、Elixir の Mix といった他エコシステムでもほぼ同じ形になっており、Bundler や NuGet もワークスペースに近い手段を提供しています。すべてのエコシステムが独自に同じ構造に至るということは、何か根本的なものが働いているとしか思えません。そこで、その理由を探ってみたいと思います。
基本的な問題
リポジトリ内に 2 つのパッケージがあり、片方がもう一方に依存しているケースを想像してください。ワークスペースがない場合、依存先を変更するたびにそのパッケージを公開し直さなければならず、あるいは手動でシンボリックリンクを張って管理する必要があります。しかし、システム全体に見えないリンクが残り、微妙に壊れたり、公開されたパッケージと挙動が異なるという問題が発生します。
ワークスペースは、インストール時にローカル依存関係を自動で結び付けます。1 つのパッケージを編集すると、もう 1 つが即座に変更を反映します。公開する際には通常通りバージョン解決が行われます。
よくあるユースケース
人はワークスペースとモノレポを同一視しがちですが、大規模コードベースでなくてもメリットがあります。代表的な例は次の通りです。
- ライブラリとそのプラグイン
- 公開されないローカルユーティリティを持つアプリ
- テスト用に例示アプリと共に動作確認するパッケージ
- デバッグ目的で依存先をローカルクローンして利用
ワークスペースは「これらのパッケージが同時開発されている」ことを解決し、モノレポは「すべてのコードが 1 か所にある」ことを解決します。両者は重なる部分がありますが、別物です。複数リポジトリ間で変更を調整するのは面倒(PR が分離、CI が個別、リリーススケジュールも異なる)ため、モノレポが魅力的になったわけです。ワークスペースは依存関係の結び付けを自動化して、モノレポを実用的にします。
実際の仕組み
npm (v7 以降)
{ "workspaces": ["packages/*"] }
npm install を実行すると node_modules に各ワークスペースパッケージへのシンボリックリンクが作られます。もし package-b が package-a を依存として宣言していれば、npm はレジストリからではなくローカルコピーへリンクします。可能な限り依存はルートの node_modules に hoist(昇格)されますが、これは後述する「幻影依存」問題を引き起こす原因になります。npm にはワークスペース専用の公開機能はありません。手動リンクの代替策として npm link が利用できます。
Yarn
Yarn は最初からワークスペースをサポートしていました。Yarn 1 がこのパターンを広め、Yarn Berry(v2 以降)では内部構造が変わりましたが同じ設定を保持しています。npm と同様に依存は hoist され、公開時のワークスペース感知機能はありません。
pnpm
-
hoisting を行わない
各パッケージに独自の
が作られ、pnpm のコンテンツアドレス可能ストアへのシンボリックリンクのみが配置されます。これにより、パッケージは明示的に宣言した依存だけをインポートできます。node_modules -
プロトコルworkspace:{ "dependencies": { "sibling-package": "workspace:*" } }これにより pnpm は常にワークスペース内で解決し、レジストリからは取得しません。公開時には
が実際のバージョン番号へ置き換えられます。npm と Yarn にはこの機能がないため、ローカルパスを参照したまま公開してしまうケースがあります。厳格な分離は依存バグを早期に発見できる一方で、npm/Yarn からの移行時に多少の摩擦があります。workspace:*
Bun
Bun は npm と Yarn と同じ
workspaces フィールドを使用し、シンボリックリンクを作成します。Bun の高速性はワークスペースインストールにも適用されます。
Cargo (Rust)
[workspace] members = ["crates/*"]
すべてのメンバーが単一の
Cargo.lock を共有し、ビルドは同じターゲットディレクトリに行われます。path = "../other" のように宣言した依存は Cargo がリンクします。共有ロックファイルはワークスペース全体で整合性を保ちます。また、機能(feature)解決もワークスペース単位で統一されるため、同じ依存の異なるバージョンが重複してビルドされることはありません。cargo publish はワークスペース関係を理解し、依存順にメンバーを公開できます。
Go
従来は
replace ディレクティブを使っていました。
replace example.com/mylib => ../mylib
これはコンパイラに対してそのインポートをローカルパスから解決するよう指示します。ディレクティブは
go.mod に記述され、明確な意図が伝わります。
Go 1.18 で導入された
go.work は複数モジュールワークスペースをサポートします。各モジュールの go.mod に個別に replace を追加する代わりに、リポジトリルートに go.work ファイルを作成します。
go 1.18 use ( ./app ./lib )
これで Go は指定されたモジュール間のインポートをローカルで解決します。主な違いは、
go.work が通常バージョン管理から除外される点です。開発時の便利機能であり、公開モジュールには含まれません。Go はレジストリを持たず(モジュールは proxy.golang.org など経由で Git から取得)、ワークスペースはネットワーク呼び出しを短縮する役割も果たします。
Bundler
Bundler に公式のワークスペース機能はありません。Gemfile でパス依存を指定します。
gem 'my_gem', path: '../my_gem'
開発時には便利ですが、公開時に Gemfile を変更する必要があります。
bundle config local を使えば Gemfile を編集せずにローカルパスへリダイレクトできますが、やはりワークスペース感知の出版機能はありません。
Composer (PHP)
Composer は path リポジトリをサポートします。
{ "repositories": [ { "type": "path", "url": "../my-package" } ] }
これによりローカルパッケージがシンボリックリンクで結び付けられます。Bundler 同様、開発時の便宜であり、ワークスペース感知の公開機能はありません。
Swift Package Manager
Xcode UI か
Package.swift を編集してローカルパッケージを指定します。
.package(path: "../MyLibrary")
Swift は Git からパッケージを取得するため、Go と同様にレジストリが存在せず、ワークスペースはネットワーク呼び出しの短縮を目的としています。
pub (Dart/Flutter)
pub もワークスペースをサポートします。ルートに
pubspec.yaml を作り、workspace フィールドでメンバーを列挙します。
name: my_workspace workspace: - packages/app - packages/shared
メンバーは共通の解決戦略を共有し、
pub get がリンクします。Dart パッケージは個別に pub.dev に公開されます。
Mix (Elixir)
エルビラプログラムで「アンブレラプロジェクト」を作成します。親プロジェクトの
mix.exs で子アプリを apps/ ディレクトリに配置します。
# mix.exs at root defmodule MyUmbrella.MixProject do use Mix.Project def project do [ apps_path: "apps", deps: deps() ] end end
各アプリは独自の
mix.exs を持ちつつ、依存関係を共有し相互参照できます。アンブレラプロジェクトは Hex に個別に公開可能です。
NuGet (.NET)
ローカル依存は「プロジェクト参照」で実現します。ソリューション内でプロジェクト同士が直接参照します。
<!-- In MyApp.csproj --> <ItemGroup> <ProjectReference Include="..\MyLibrary\MyLibrary.csproj" /> </ItemGroup>
集中管理のために
Directory.Packages.props を使ってバージョンを共有できます。NuGet.org への公開はパッケージ単位です。
よくある問題点
| 問題 | 説明 |
|---|---|
| 幻影依存(Phantom dependencies) | npm と Yarn は可能な限り依存をルート に hoist します。これにより、隣接パッケージが宣言した依存を自動的にインポートできるようになります。しかしワークスペース外で公開されたパッケージはその依存を持たないため、別環境で失敗します。pnpm は hoist しないのでこの問題を回避できます。 |
| バージョン不一致 | ワークスペース内では のような制約はローカルディスク上の実際のパッケージに対して無視されます。公開時に初めてバージョン不整合が露呈します。 |
| ツールの仮定 | Jest、TypeScript、ESLint など多くの開発ツールはワークスペース構成を認識できるよう設定が必要です。シンボリックリンクを正しく扱うものとそうでないものがあります。結果として、ツール専用の設定ファイルが増えます。 |
| CI とローカルの差異 | ローカル開発時に hoist された依存は CI 環境では別解決になる可能性があります。新規インストールでは同じ結果にならないケースがあります。 |
| ビルドオーケストレーション | ワークスペースはコードの場所を統一するだけで、ビルド順序は管理しません。TypeScript などのコンパイルが必要なパッケージ間では、依存先を先にビルドしておかないと型情報が取れません。そのため Turborepo や Nx のようなツールが追加で必要になることがあります。 |
| 公開調整 | ワークスペースは開発時のリンクのみを扱い、パッケージ公開は別問題です。同じ変更セットで複数パッケージを同時にリリースしたい場合、バージョン番号を手動で合わせる必要があります。Changesets(JavaScript 専用)や Lerna の などがこの調整を補助します。Cargo は依存順に公開できますが、バージョニングは手作業です。 |
| レジストリの制約 | npm/Yarn ではスコープ(例: )で名前空間化できますが、レジストリ側には「これらパッケージは一緒に管理される」という概念はありません。個別に公開し、消費者側で同時更新を保証する必要があります。 |
結論
すべてのエコシステムはパッケージ作成を極めて簡単にしましたが、その結果として多数の小さなパッケージが増え、調整コストが高くなりました。ワークスペースは「同時開発」する複数パッケージ間のリンクと依存解決を自動化し、この摩擦を軽減します。
私自身はワークスペースを使った経験がないため、実際に導入した方から聞きたいことがあります。どんなケースでワークスペースに乗り換えましたか?メリットとデメリットは何でしたか?ぜひ Mastodon などで共有してください。