現在所属しているチームでは、かつてはbowerを用いJSライブラリを管理していたが、最近は browserify の導入に伴い npm への移行を進めている。
新たにパッケージをインストールして npm-shrinkwrap.json を更新する際、他のパッケージの from
フィールドが更新される事があった。
npm-shrinkwrap.json を調べるついでに、せっかくなので npm のコードをちょっとだけ読んでみた。
npm-shrinkwrap.json って?
Node.js のパッケージマネージャ npm には、プロジェクトの依存パッケージを管理する機能がある。
npm install --save
or npm install --save-dev
でパッケージをインストールすると、package.json 内の dependencies
or devDependencies
フィールドにパッケージ情報が追加される。
package.json では、依存パッケージのバージョンを固定することは出来るが、「依存パッケージの依存パッケージ」のバージョンを固定する事はできない。
例えば、プロジェクト A
の依存パッケージを "B": "0.1.0"
として固定しても、B
の package.json が "C": "*"
となっていたら、Cが publish される度に新しいバージョンがインストールされてしまう。
そのため、より厳密にバージョンを固定したいときは npm-shrinkwrap.json を使う。
(Gemfile.lock や cpanfile.snapshot のようなもの??)
from フィールドが更新される
さて、表題の件。
新しいパッケージをインストールして npm-shrinkwrap.json を更新する時、既にインストール済みの他パッケージの from
フィールドまで更新されてしまうことがあった。
具体的には、"from": "from@*"
が "from": "(リポジトリのURL)"
に変更されていた。
フィールド名からして、おそらく
- 最初に
npm i -S hoge@x.y.z
とした時は、from
に指定されたバージョン番号を、resolved
にリポジトリのURLを記録する - ↑で作成した npm-shrinkwrap.json を用い
npm install
する時は、resolved
に記録されたURLからパッケージをインストールし、from
を更新する
と想像はできたが、確証が持てなかったのでコードを追ってみた。
現時点でのlatest stableは 101190a4f2
だ。
https://github.com/npm/npm/tree/101190a4f27510d1de988c7f598d7c3bbea6ca8a
lib/shrinkwrap.js
npm shrinkwrap
のソースコードはこちら
https://github.com/npm/npm/blob/101190a4f27510d1de988c7f598d7c3bbea6ca8a/lib/shrinkwrap.js
from
, resolved
で検索してもそれらしい箇所は見当たらない。
となると、npm.commands.ls()
の時点で既に from
, resolved
が設定される気がする。
プロジェクトのディレクトリからREPLを開いて試してみる。
> var npm = require('npm'); > npm.load(); > var pkginfo; npm.commands.ls([], true, function(er, _, pkginfo_){ pkginfo = pkginfo_; }); > pkginfo { name: 'yo', version: '1.0.0', dependencies: { yay: { version: '0.1.0', from: 'yay@>=0.1.0 <0.2.0', resolved: 'https://registry.npmjs.org/yay/-/yay-0.1.0.tgz' } } }
やはり。
今度は npm ls
の方で、node_modules
を読み込んだ結果を出力してみる。
https://github.com/npm/npm/blob/101190a4f27510d1de988c7f598d7c3bbea6ca8a/lib/ls.js#L46
こうして
var bfs = bfsify(data, args) , lite = getLite(bfs) console.log(data); // 出力してみる if (er || silent) return cb(er, data, lite)
こう
$ npm ls | grep _from _from: 'https://registry.npmjs.org/yay/-/yay-0.1.0.tgz',
まだ加工してないのに _from
, _resolved
がある、ということは、npm install
した時点で _from
, _resolved
フィールドが作られてるのか。
node_modules/
下にあるリポジトリの package.json みたら既に _from
, _resolved
が存在した……。
lib/install.js
というわけで、インストール時にどうやって _from
, _resolved
が作られるのか調べたい。
npm install
のコードを見てみる。
https://github.com/npm/npm/blob/101190a4f27510d1de988c7f598d7c3bbea6ca8a/lib/install.js
あるパッケージを npm install hoge
した時の流れはこんな感じかな
install -> installManyTop -> installManyTop_ -> installMany
installManyの中で呼ばれてる targetResolver
って奴が怪しそう。
targetResolverが返す値をみてみよう。
705行目あたりでこうして
asyncMap( what , targetResolver(where, context, deps, devDeps) , function (er, targets) { console.log(targets); // 出力してみる
こう
$ rm -rf node_modules && npm cache clean && npm i -S yay [ { name: 'yay', version: '0.1.0', description: 'Generate random, ridiculous names for anything. Yay!', main: 'index.js', scripts: { test: 'echo "Error: no test specified" && exit 1' }, repository: { type: 'git', url: 'https://github.com/divshot/yay.git' }, keywords: [ 'divshot', 'superstatic', 'names', 'generator', 'silly', 'random' ], author: { name: 'Divshot' }, license: 'MIT', bugs: { url: 'https://github.com/divshot/yay/issues' }, homepage: 'https://github.com/divshot/yay', _id: 'yay@0.1.0', dist: { shasum: '083dff9823620a4b7dc95461d9c22bf70eb45305', tarball: 'http://registry.npmjs.org/yay/-/yay-0.1.0.tgz' }, _from: 'yay@>=0.1.0 <0.2.0', _npmVersion: '1.4.3', _npmUser: { name: 'scottcorgan', email: 'scottcorgan@gmail.com' }, maintainers: [ [Object] ], directories: {}, _shasum: '083dff9823620a4b7dc95461d9c22bf70eb45305', _resolved: 'https://registry.npmjs.org/yay/-/yay-0.1.0.tgz' } ]
既に _from
, _resolved
があることがわかる。
targetResolver は asyncMap に渡す関数を作って返す。
targetResolver が返す resolver のシグネチャはこんな感じ (837行目) 。
return function resolver (what, cb) {
asyncMap はちょっと変な map だ。
配列の各要素に関数を適用するのは同じだが、その関数の2番目の引数に渡されるコールバックに結果を返す。
asyncMap([1, 2, 3], (x, cb) => cb(null, x * 100), (err, data) => console.log(data)) // [100, 200, 300]
なので、今回は cb()
に渡される2番目の引数を見れば良い。
cb()
を呼び出している箇所のうち、2番目の引数をちゃんと渡してるのは910行目だけ。
ここで渡してる data
は、cache.add のコールバックの引数として与えられるものだ。
cache.add(what, null, pkgroot, false, function (er, data) { if (er && parent && parent.optionalDependencies && parent.optionalDependencies.hasOwnProperty(npa(what).name)) { log.warn("optional dep failed, continuing", what) log.verbose("optional dep failed, continuing", [what, er]) return cb(null, []) } var type = npa(what).type var isGit = type === "git" || type === "hosted" if (!er && data && !context.explicit && context.family[data.name] === data.version && !npm.config.get("force") && !isGit) { log.info("already installed", data.name + "@" + data.version) return cb(null, []) } if (data && !data._from) data._from = what if (er && parent && parent.name) er.parent = parent.name return cb(er, data || []) })
cache.js , cache/add-named.js, cache/add-remote-tarball.js
https://github.com/npm/npm/blob/101190a4f27510d1de988c7f598d7c3bbea6ca8a/lib/cache.js
cache.add()
がやってることは単純だ。
- realizePackageSpecifier()
で、どの方法でインストールするかの情報を取得する
- インストール方法によって addLocal()
, addRemoteTarball()
, addRemoteGit()
, addNamed()
のどれかを呼ぶ (279行目あたり)。
realizePackageSpecifier(spec, where, function (err, p) { if (err) return cb(err) log.silly("cache add", "parsed spec", p) switch (p.type) { case "local": case "directory": addLocal(p, null, cb) break case "remote": // get auth, if possible mapToRegistry(spec, npm.config, function (err, uri, auth) { if (err) return cb(err) addRemoteTarball(p.spec, {name : p.name}, null, auth, cb) }) break case "git": case "hosted": addRemoteGit(p.rawSpec, cb) break default: if (p.name) return addNamed(p.name, p.spec, null, cb) cb(new Error("couldn't figure out how to install " + spec)) } })
コンソールから npm install hoge
or npm install hoge@x.y.z
とした場合は addNamed()
が呼ばれるので、add-named.js を見てみる。
https://github.com/npm/npm/blob/101190a4f27510d1de988c7f598d7c3bbea6ca8a/lib/cache/add-named.js
addNamed()
はこれまた、addNameVersion()
, addNameRange()
, addNameTag()
に分岐する……のだが、どれもやることは大体おなじで、バージョン番号など必要な情報を取得したのち自分自身を呼び直している。
addNameVersion()
では、data
が truthy なら fetchit()
でパッケージをダウンロードし、data
が falsy なら getOnceFromRegistry()
を呼び、パッケージのメタ情報を取得してからもう一度 fetchit()
を呼ぶ。
試しに29行目でデータを出力してみると、無事パッケージ tarball のURLが入った JSON データが出力された。
function getOnceFromRegistry (name, from, next, done) { function fixName(err, data, json, resp) { // (中略) console.log(json); // 出力してみる next(err, data, json, resp) } // (中略) }
(ちなみに npm cache clean
しないと resp.statusCode === 304
になって結果返してくれない)
fetchit
では、得られた tarball のURLに対し addRemoteTarball()
を呼ぶ。
addRemoteTarball()
を見ると、_from
, _resolved
に値を入れている事がわかる!!!!!
https://github.com/npm/npm/blob/101190a4f27510d1de988c7f598d7c3bbea6ca8a/lib/cache/add-remote-tarball.js#L19-26
function cb (er, data) { if (data) { data._from = u data._resolved = u data._shasum = data._shasum || shasum } cb_(er, data) }
あれ……??
これでは npm install -S hoge
とした場合にも _from
, _resolved
は両方とも tarball のURLになるはずでは……??
とおもいきや、addNamed()
から渡される cb_()
の中で _from
だけ hoge@x.y.z
形式になるよう再代入されているのだった。
https://github.com/npm/npm/blob/101190a4f27510d1de988c7f598d7c3bbea6ca8a/lib/cache/add-named.js#L52
addNamed()
が完了すると、cache.js 側で afterAdd()
が呼ばれる。
これによって、_from
, _resolved
を持った data
が package.json に記録される。
というわけで、npm install hoge
した場合に from
, resolved
が記録される仕組みがわかった!!!!!!!!
npm-shrinkwrap.json からインストールした場合
npm-shrinkwrap.json からインストールした場合についても既にわかっている。
cache.add()
において realizePackageSpecifier()
を行うことは説明したが、 npm-shrinkwrap.json
がある場合には type: 'remote'
となるため、addNamed()
を経由せず、直接 addRemoteTarball()
を呼び出す。
結果、addNamed()
で _from
を再代入する処理がスルーされ、_from
には tarball のURLが記録されるのだった。
おまけ : インストール方法による realizePackageSpecifier()
結果の違い
npm i -S yay@0.1.0
{ raw: 'yay@0.1.0', scope: null, name: 'yay', rawSpec: '0.1.0', spec: '0.1.0', type: 'version' }
npm i
(package.json からインストール)
{ raw: 'yay@^0.1.0', scope: null, name: 'yay', rawSpec: '^0.1.0', spec: '>=0.1.0 <0.2.0', type: 'range' }
npm i
(npm-shrinkwrap.json からインストール)
{ raw: 'yay@https://registry.npmjs.org/yay/-/yay-0.1.0.tgz', scope: null, name: 'yay', rawSpec: 'https://registry.npmjs.org/yay/-/yay-0.1.0.tgz', spec: 'https://registry.npmjs.org/yay/-/yay-0.1.0.tgz', type: 'remote' }
better npm-shrinkwrap
npm-shrinkwrapの代替となるラッパー?をみつけたので紹介。
uber/npm-shrinkwrap
npm shrinkwrap
との違いは以下:
- package.json, npm-shrinkwrap.json, node_modules の一貫性を保証する
- 素の
npm shrinkwrap
では、--save
のし忘れ等で矛盾が発生したら叱ってくれるが、package.json の tag が変更されても叱ってくれない
- 素の
npm cache clean
してくれる- まれに cache が原因でエラー出まくるのを防ぐ
- npm-shrinkwrap.json の resolved フィールドを固定し、from フィールドを削除する
- 変な diff が出ないようにしてくれる
- プログラマブルな設定
どういう仕組で動いてるか解説してくれてuberは親切だな〜〜〜〜
あと、APIの型とかをOCamlのファイル?で宣言してある。
OCamlのファイル使って型チェックするような仕組みあるのかな?見つけられなかった
mozilla/npm-lockdown
npm shrinkwrap
との違いは2つ:
あとマスコットキャラがかわいい。
mozilla/npm-seal
https://github.com/mozilla/npm-lockdown
npm-lockdownのsha1チェック機能だけ版
マスコットキャラかわい
その他npmパッケージ
iarna/aproba
シンプルなバリデーションライブラリ
iarna/write-file-atomic
fs.writeFile()
のアトミック版。書き込み中にエラーが起きたらファイルを削除してくれる
あと uid/gid も指定できる
npm/slide-flow-control
シンプルな実行フロー制御ライブラリ
- asyncMap : mapした関数の実行が全て終わったあとのコールバックを渡せる
- chain : async の series みたいな感じ
shesek/iferr
はい
npmで使われるようなライブラリでも、あんまり☆つかないんだな〜〜〜〜