package.jsonの(dev)dependencies肥満化に向き合ってみる
これは「Node.js Advent Calendar 2015」の17日目のエントリです。
長いので一行まとめ
npmのLocal Paths + linklocalを使ってモジュール管理すると(dev)dependenciesがスッキリ
さて、以下本題です。
(dev)dependenciesの肥満化問題
突然ですが、WebサービスをクライアントもサーバもJavaScriptで書く場面を想像してください。
最近だと、クライアント側のコードもBrowserifyやWebpackを使って、package.jsonでモジュール管理するのが一般的ではないかと思います。
そして、サーバ側もNode.jsで書かれているのでpackage.jsonでモジュール管理します。
すると、package.jsonが↓みたいなことになります(wp-calypsoのpackage.json)
{ "dependencies": { "async": "0.9.0", "atob": "1.1.2", "autoprefixer": "4.0.0", "autosize": "3.0.7", "babel": "5.8.12", "babel-core": "5.8.12", "babel-loader": "5.3.2", "babel-runtime": "5.8.12", "body-parser": "^1.13.3", "browser-filesaver": "1.1.0", "chalk": "1.0.0", "char-spinner": "1.0.1", "chrono-node": "^1.0.6", "classnames": "1.1.1", "click-outside": "1.0.4", "clipboard": "1.5.3", "commander": "2.3.0", "component-classes": "1.2.1", "component-closest": "0.1.4", "component-tip": "2.5.0", "component-uid": "0.0.2", "cookie": "0.1.2", "cookie-parser": "1.3.2", "creditcards": "1.1.1", "debug": "2.2.0", "dom-scroll-into-view": "1.0.1", "email-validator": "1.0.1", "emitter-component": "1.1.1", "escape-regexp": "0.0.1", "escape-string-regexp": "1.0.3", "events": "1.0.2", "exports-loader": "0.6.2", "express": "4.13.3", "flux": "2.1.1", "he": "0.5.0", "html-loader": "0.2.3", "immutable": "3.7.5", "imports-loader": "0.6.3", "inherits": "2.0.1", "jade": "jadejs/jade#29784fd", "jed": "1.0.2", "json-loader": "0.5.1", "jstimezonedetect": "1.0.5", "key-mirror": "1.0.1", "keymaster": "1.6.2", "lodash": "3.10.1", "lunr": "0.5.7", "marked": "0.3.5", "moment": "2.10.6", "moment-timezone": "^0.4.0", "morgan": "1.2.0", "node-sass": "3.3.3", "page": "1.6.1", "phone": "git+https://github.com/Automattic/node-phone.git#1.0.4-6", "photon": "1.0.4", "q": "1.0.1", "qrcode.react": "0.4.1", "qs": "4.0.0", "r-dom": "1.3.0", "raf": "2.0.4", "react": "0.13.3", "react-day-picker": "1.1.0", "react-redux": "3.1.0", "react-tap-event-plugin": "0.1.6", "redux": "3.0.4", "redux-analytics": "0.3.0", "redux-thunk": "1.0.0", "rtlcss": "1.6.1", "sanitize-html": "1.11.1", "shallowequal": "0.2.2", "source-map": "0.1.39", "source-map-support": "0.3.2", "store": "1.3.16", "superagent": "1.2.0", "timeout-transition-group": "1.0.3", "tinymce": "4.2.8", "to-title-case": "0.1.5", "tween.js": "16.3.1", "twemoji": "1.3.2", "uglify-js": "2.6.1", "walk": "2.3.4", "webpack": "1.12.6", "webpack-dev-middleware": "1.2.0", "wpcom-unpublished": "1.0.7", "wpcom-proxy-request": "1.0.5", "wpcom-xhr-request": "0.3.3", "xgettext-js": "0.2.0" }, "devDependencies": { "babel-eslint": "4.1.1", "better-assert": "1.0.2", "blanket": "1.1.6", "chai": "2.0.0", "chai-immutable": "^1.4.0", "enzyme": "1.1.0", "esformatter": "0.7.3", "esformatter-braces": "1.2.1", "esformatter-collapse-objects-a8c": "0.1.0", "esformatter-dot-notation": "1.3.1", "esformatter-quotes": "1.0.3", "esformatter-semicolons": "1.1.1", "esformatter-special-bangs": "1.0.1", "eslint": "1.3.1", "eslint-plugin-react": "3.3.1", "jsdom": "3.1.2", "localStorage": "1.0.2", "lodash-deep": "1.5.3", "mixedindentlint": "1.1.1", "mocha": "2.3.4", "mockery": "1.4.0", "nock": "2.17.0", "react-addons-test-utils": "0.14.3", "react-hot-loader": "1.3.0", "rewire": "2.3.3", "sinon": "1.12.2", "sinon-chai": "2.7.0", "socket.io": "1.3.7", "supertest": "^1.1.0", "webpack-dev-server": "1.11.0" } }
スクロールをいっぱいさせてしまい申し訳ございません。
さすがにこの数だと、クライアント・サーバどちらが何のモジュールに依存しているのかパッと理解するのは難しいですよね?
どうしよう?
これでも気にならない・・・のならいいのですが、可能であれば、理解し易いように整理したいです。
何か解決策はないか考えてみました。
解決策1: プロジェクトを分割し、別リポジトリに切り出してみては?
解決策の1つとしてぱっと思いつくのは、クライアントとサーバのコードを別リポジトリに切り出し、それぞれを独立したnpmモジュールとして管理することです。
ProjectX/system-x ← system-x-clientとsystem-x-serverをrequire()して使う ProjectX/system-x-client ← クライアントのコード ProjectX/system-x-server ← サーバのコード
・・・が、書いておいてアレなんですが、開発時のオーバーヘッドが大きそうなので、できればこれは避けたいです。
例えば
- 1機能追加する度に、
system-x-{client,server}
を修正し、system-x
でバージョンを調整するのめんどくない? - 複数人で開発する場合はどうする?
npm link
だけで頑張れる?- コードレビューはどうする?
等々、いろいろと検討しなければならなさそうです。
クライアントとサーバでチームが分かれるようなタイプの開発でなら検討してもよいかもしれませんが、自分のいるチームの開発体制には合わなさそうでした。
解決策2: Local Pathsで管理すればよいのでは?
npmではv2系からLocal Pathsという機能があり、dependencies
にローカルなパスを指定できるようになっています。
(公式:https://docs.npmjs.com/files/package.json#local-paths)
system-x/ ├── client │ └── package.json ├── package.json └── server └── package.json
な感じに、client、serverの各ディレクトリにpackage.jsonを作成し、npm install -S client/ server/
すると
↓のようなpackage.jsonが出来上がります。
"dependencies": { "client": "file:client", "server": "file:server" },
要はfile:・・・
と指定することで、リポジトリ内のディレクトリ毎に別のnpmモジュールとして管理する・・・なんてことができます。
この機能を使えば、リポジトリは分けずに、package.jsonはキレイに分割できそうです。
開発時に気にすることも特に増えなさそう。
よかった、よかった・・・
ところが
これで問題解決かと思いきや、実はまだ問題が。
Local Pathsで管理しているモジュールも通常のnpmモジュールと同様に、
ソースコードを修正した場合は、node_modules
配下に再インストールしないといけないのです。
要は
- clientを修正→
npm install client
- serverを修正→
npm install server
みたいなことをいちいちしないといけないと。
npm link
みたいにシンボリックリンクを貼って、ごにょごにょできる仕組みがあれば、良かったのですが、そんな仕組みも特になさげでした。
watchして自動インストールみたいな仕組みも作れなくはないですが、待ちが発生して開発リズムが悪くなりそうなのも嫌ですね。
そこでlinklocal
そこで、使えるのが
このlinklocalはnode_modules配下にLocal Pathsで管理されているモジュールのシンボリックリンクを貼ってくれるツールです。
Local Paths用npm link
という説明が一番わかりやすいかもしれません。
clientの修正はnode_modules/client、serverの修正はnode_modules/serverに即座に反映されるので、これなら、いい感じに開発できます。
linklocalの使い方
インストール方法
npm install -D linklocal
でpackage.jsonのdevDependenciesにも足しておきます。
シンボリックリンクを貼る
# これが ❯ ls -l node_modules/ total 0 drwxr-xr-x 4 aaa wheel 136 12 16 19:14 client drwxr-xr-x 12 aaa wheel 408 12 16 19:14 linklocal drwxr-xr-x 5 aaa wheel 170 12 16 19:14 server # linklocalを使うと ❯ ./node_modules/.bin/linklocal client server Linked 2 dependencies # シンボリックリンクが貼られる ❯ ls -l node_modules/ total 16 lrwxr-xr-x 1 aaa wheel 9 12 16 19:14 client -> ../client drwxr-xr-x 12 aaa wheel 408 12 16 19:14 linklocal lrwxr-xr-x 1 aaa wheel 9 12 16 19:14 server -> ../server
注意点
残念なことに、linklocalしただけでは各モジュール内でnpm install
されないので、
ここは手動でやらねばなりません。
作者はbulk
というモジュールを用いて、自動的にインストールされるようにする方法を紹介していました。
{ "scripts": { "dev": "linklocal link -r && linklocal list -r | bulk -c 'npm install --production'", "prepublish": "if [ \"$NODE_ENV\" != \"production\" ]; then npm run dev; fi" } }
自分の場合は↓のrun-script
を準備しました。
{ "scripts": { "prelinklocal": "rm -rf ./node_modules/{client,server}", "linklocal": "linklocal link", "postlinklocal": "for d in $(linklocal list); do (cd $d && npm i); done" }
↓を打てば、シンボリックリンクを貼れます。
npm run linklocal
まとめ
まだ、試行錯誤段階ですが、今のところ、Local Paths + linklocalを使った開発で上手くいっているところです。
また、何かわかったことがあれば、続報を書くかも知れません。
それでは、最後まで読んでいただき、ありがとうございました。
本記事が、皆様の開発の何かしらのヒントになれば幸いです。