Go to list of users who liked
Share on X(Twitter)
Share on Facebook
More than 3 years have passed since last update.
npm workspace の使い方
npm v7 で追加された workspace 機能の使い方について紹介します。
記事中で使用している npm のバージョンはv7.22.0 です。
workspace 機能とは
yarn workspace のような機能です。
単一のルートパッケージから複数のパッケージを workspace として管理することができます。
つまり、次のような monorepo を管理するための機能です。
.├── package.json└── packages ├── a │ └── package.json └── b └── package.jsonworkspace 機能を使うことによって、package-a,package-b のような複数のパッケージをトップレベルの npm プロジェクト (トップレベルのpackage.json) から管理・操作することができます。
ちなみに、このような monorepo 管理を支援するツールとしてはlerna が有名ですが、2021年 2 月のv4.0.0 リリース以降、開発があまり活発ではないようです(2021 年 8 月時点)。
今後は「脱 lerna」が進んで yarn workspace や npm workspace のみで monorepo を管理する方が主流になるかもしれません。
workspace を作る
まずはルートとなるトップレベルの npm プロジェクトを作成します。
$mkdirsample$cdsample$npm initルートの npm プロジェクトは workspace の管理用なので公開されることはありません。
生成されたpackage.json を編集してprivate フィールド を true にしておくと良いでしょう。
{"name":"sample",// ..."private":true}workspace を追加するには-w オプションをつけてnpm init を呼び出します。
$npm init-w packages/a上記の場合、packages/a ディレクトリが新しい workspace として追加されます。packages/a/package.json が作成されたことが確認できるはずです。
$tree.├── package.json└── packages └── a └── package.jsonまた、ルートのpackage.json にはworkspaces フィールド が自動で追加されています。
{"name":"sample","version":"1.0.0",// ..."private":true,"workspaces":["packages/a"]}npm init -w は「workspace となるディレクトリの作成」、「workspace 用のpackage.json の生成」、「ルートのpackage.json のworkspace フィールドの更新」を行うだけなので手動でこれらの作業を行っても構いません。
もう 1 つ workspace を追加してみましょう。
$npm init-w packages/b$tree.├── package.json└── packages ├── a │ └── package.json └── b └── package.jsonルートのpackage.json は次のようになるはずです。
{"name":"sample","version":"1.0.0",// ..."private":true,"workspaces":["packages/a","packages/b"]}ちなみにworkspaces フィールドはパターンも受け付けるので、上記のようなディレクトリ構成の場合はパターンに書き換えてしまった方がすっきりします。
{"name":"sample","version":"1.0.0",// ..."private":true,"workspaces":["packages/*"]}これで workspace を使う準備ができました。npm install を実行してみましょう。
$npminstall生成されたnode_modules 以下に、先ほど作成した 2 つの workspace のシンボリックリンクが作成されます。
$tree node_modules/node_modules/├── a ->../packages/a└── b ->../packages/bこれによって、各 workspace をプロジェクト内から参照できるようになるという仕組みです。
試しに workspaceb から workspacea を参照してみましょう。
workspacea 側で適当に文字列を export します。
module.exports="workspace a"workspaceb 側に modulea を参照するコードを作成し、実行してみます。
consta=require("a");console.log(a);$node packages/b/index.jsworkspace aworkspacea が workspaceb から参照できることが確認できました。
依存パッケージを追加する
(npm workspace に限った話ではありませんが) monorepo では一般的に次のように依存パッケージを管理します。
- 開発用の依存パッケージ (
devDependencies) はルートパッケージのpackage.jsonで管理する - サブパッケージのアプリケーションコードが参照する依存パッケージはサブパッケージ内の
package.jsonで管理する
このようにすることで、複数パッケージの開発に使うツールのバージョンや設定を単一のルート設定で共通化しつつ、各サブパッケージの依存関係は個々に管理することができます。
また、開発用の依存パッケージをルート側にのみインストールすることでディスクスペースの節約にもなります。
これを踏まえて依存パッケージを追加してみましょう。
eslint のような lint ツールは開発用の依存パッケージなので単純にルート側でnpm install を行います。
$npminstall--save-dev eslintworkspace のコードが依存するパッケージは、次のように-w をつけてルート側 でnpm install を実行して追加します。
$npminstall-w packages/a --save node-fetchこうすることにより、workspace が依存するパッケージもルートのpackage-lock.json によって管理されるようになります。
これにより新規に clone したリポジトリでセットアップを行う場合でも、ルート側でnpm install を実行するだけで全ての workspace の依存パッケージを取得することができます。
また、他の workspace が依存している同一のパッケージと競合せずにバージョン解決できる場合はルート側のnode_modules にインストールされ、workspace 間で共有されます(hoisting)。
バージョンが競合する場合は workspace 内のnode_modules にインストールされる仕組みです。
次のように workspace 内で単純にnpm install を実行してはいけないことに注意してください。
$cdpackages/a$npminstall--save node-fetchこの方法だとpackages/a が workspace ではなく単一のパッケージとみなされてしまうため、新規にpackages/a/package-lock.json が生成され、ルートのpackage-lock.json で管理することができなくなってしまいます。
workspace で定義された npm script を実行する
特定の workspace で定義された npm script は、ルート側でnpm run に-w オプションをつけて workspace を指定することで実行することができます。
例えば次のように workspacea にprint という npm script を定義したとします。
{"name":"a","version":"1.0.0",// ..."scripts":{"print":"echo\"workspace a\""}}この npm script はルート側から次のように実行できます。
$npm run print-w packages/a>a@1.0.0 print>echo"workspace a"workspace aまた、--workspaces オプションをつけてnpm run を実行すると全ての workspace で定義された npm script を一括で実行することができます。
次のように workspaceb にもprint を定義し、
{"name":"b","version":"1.0.0",// ..."scripts":{"print":"echo\"workspace b\""}}--workspaces をつけて実行すると、
$npm run--workspaces print>a@1.0.0 print>echo"workspace a"workspace a>b@1.0.0 print>echo"workspace b"workspace b全ての workspace のprint が実行されました。
例えば、次のようにルートのpackage.json に各 workspace のbuild を一括で実行する npm script を定義するといった使い方が便利です。
{"name":"sample","version":"1.0.0",// ..."workspaces":["packages/*"],"scripts":{"build":"npm run build --workspaces"}}実行対象の npm script が定義されていない workspace があるとエラーになるので注意してください。--if-present オプションをつけるとこのようなケースでもエラーにならずに実行することができます。
$npm run print--workspaces--if-present>a@1.0.0 print>echo"workspace a"workspace aまた、npm v7 では npm script から呼び出したコマンドが直近のnode_modules/.bin 以下に存在しない場合、さらに上位のディレクトリのnode_modules からコマンドを探して実行するようになっています。
これにより、workspace 側で定義した npm script はルートのnode_modules にインストールされたコマンドを呼び出すことができます。
例えば次のような場合、workspacea の依存関係にはeslint は含まれていないのですがルート側の依存関係には含まれているため workspacea で定義されている npm scriptlint からeslint を呼び出すことができます。
{"name":"sample","version":"1.0.0",// ..."workspaces":["packages/*"],"devDependencies":{"eslint":"^7.32.0"}}{"name":"a","version":"1.0.0",// ..."scripts":{"print":"echo\"workspace a\"","lint":"eslint ."},"dependencies":{"node-fetch":"^2.6.1"}}workspace のnode_modules/.bin にコマンドが存在する場合はそちらが優先されるため、workspace 単位でツールのバージョンを使い分けることもできます。
{"name":"b","version":"1.0.0",// ..."scripts":{"lint":"eslint ."},"devDependencies":{"eslint":"^6.8.0"}}$npm run-w packages/b lint そのほかの操作
npm outdated やnpm publish など、多くの npm サブコマンドに workspace サポートが追加されています。
詳細は各コマンドの公式ドキュメントを確認してください。
npm workspace による monorepo 運用の Tips
依存関係の落とし穴に対処する
npm workspace で管理しているサブパッケージを npm として公開したい場合は、workspace 側の依存関係に注意しないと思わぬ落とし穴に引っかかることがあります。
例えばpackage-a を npm workspace で管理しているとします。
このpackage-a はnode-fetch に依存しているものとします。
{"name":"package-a","version":"1.0.0",// ..."dependencies":{"node-fetch":"^2.6.1"}}npm install を実行するとnode-fetch はルート側のnode_modules にインストールされます。
次のような状態です。
.├── node_modules│ ├── node-fetch│ └── package-a -> ../package-a├── package-a│ └── package.json├── package-lock.json└── package.jsonここに、新たにnode-fetch に依存した別のパッケージpackage-b を workspace として追加しましたが、package-b のpackage.json にはnode-fetch を記載し忘れてしまいました。
{"name":"package-b","version":"1.0.0"}.├── node_modules│ ├── node-fetch│ ├── package-a -> ../package-a│ └── package-b -> ../package-b├── package-a│ └── package.json├── package-b│ └── package.json├── package-lock.json└── package.json問題なのはこのケースにおいて、すでにルートのnode_modules にnode-fetch が存在するためpackage-b からnode-fetch を参照できてしまうという点です。
この場合、ローカルでのテストや lint は成功しますが、npm として公開したpackage-b の依存関係からはnode-fetch が不足しているためユーザー側では正常に動作しなくなってしまいます。
よくやりがちなミスであるにも関わらず致命的なバグを引き起こし、CI でも検出できないという非常に厄介な問題です。
対策の 1 つとして、depcheck というツールを CI で実行するという方法があります。
depcheck はコードの実際の依存関係を検出し、package.json に記述された依存関係の過不足をチェックしてくれるツールです。
CI で全ての workspace に対してdepcheck を実行するようにすれば、依存関係の追加漏れを検出することができ前述の問題を回避することができます。
各 workspace の package.json を lint する
管理する workspace が多くなると、それだけpackage.json が増えていくことになります。
多数のpackage.json を作成・管理しているとフィールドの追加忘れや更新漏れが発生しがちです。
npm-package-json-lint というツールを CI で実行するようにしておくとこういったミスをかなり防ぐことができます。
次のような設定ファイルでpackage.json が満たすべきルールを定めて lint することができます。
{"rules":{"require-description":"error","require-engines":"error","require-license":"error","require-name":"error","require-version":"error","description-format":["error",{"requireCapitalFirstLetter":true,"requireEndingPeriod":true}],"name-format":"error","version-format":"error"}}この例では、description,engines,license,name,version を必須フィールドとし、description,name,version が正しいフォーマットであることをチェックするようにしています。
便利なツールですが、autofix 機能が未実装なのが玉に瑕です。
Dependabot で依存パッケージを自動更新する
Dependabot を使う場合、ルートのpackage.json のみを対象に設定を作成してあげれば、各 workspace のpackage.json に記載された依存関係も更新してくれるようになります。
version:2updates:-package-ecosystem:"npm"directory:"/"schedule:interval:"daily"TypeScript を使う
あまり詳しくは触れませんが、npm workspace で TypeScript を使う場合は Project References を使うと良いでしょう。
Register as a new user and use Qiita more conveniently
- You get articles that match your needs
- You can efficiently read back useful information
- You can use dark theme
