なっく日報

技術やら生活やらのメモ

package.jsonの(dev)dependencies肥満化に向き合ってみる

これは「Node.js Advent Calendar 2015」の17日目のエントリです。

qiita.com

長いので一行まとめ

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

そこで、使えるのが

github.com

この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を使った開発で上手くいっているところです。

また、何かわかったことがあれば、続報を書くかも知れません。

それでは、最後まで読んでいただき、ありがとうございました。

本記事が、皆様の開発の何かしらのヒントになれば幸いです。