C:hoge.txt
は絶対パスでしょうか。答えはNOです。
これはCドライブのカレントディレクトリに対する相対パスを表します。絶対パスならドライブ文字、コロンに続いてバックスラッシュを付けなければなりません。
Naming Files, Paths, and Namespaces (Windows)
大抵はドライブごとのカレントディレクトリなんて保持していないでしょうから、これが真面目に活躍するのはコマンドプロンプトぐらいのものです。と言いつつ、実はWinAPIのGetFullPathName関数やそれを使った.NETのPath.GetFullPathメソッドがサポートしています。
Node.js の path.resolve()
はそれに加えて相対パスの基準となるパスを複数指定できるので、
という仕様です。ソースコードはこの辺。
で、本題に入ります。
ルート直下に入れた gulp を直接起動できない
つまりこういう package.json が R:\
直下に置いてあって、npm start
で gulp を起動しようとしたのです。PATHの通ったコマンド増やさなくて済むので私は好きな方法です。
{ "name": "hoge", "version": "1.0.0", "description": "", "dependencies": { "gulp": "^3.9.1", }, "devDependencies": {}, "scripts": { "start": "gulp" }, "author": "", "license": "ISC" }
そしたらこんなエラー。
R:\>npm start > hoge@1.0.0 start R:\ > gulp [22:41:55] Using gulpfile R:\gulpfile.js module.js:472 throw err; ^ Error: Cannot find module 'R:node_modules\gulp\index.js' at Function.Module._resolveFilename (module.js:470:15) at Function.Module._load (module.js:418:25) at Module.require (module.js:498:17) at require (internal/module.js:20:19) at Liftoff.handleArguments (R:\node_modules\gulp\bin\gulp.js:121:18) at Liftoff.<anonymous> (R:\node_modules\liftoff\index.js:199:16) at module.exports (R:\node_modules\flagged-respawn\index.js:17:3) at Liftoff.<anonymous> (R:\node_modules\liftoff\index.js:191:9) at R:\node_modules\liftoff\index.js:165:9 at R:\node_modules\v8flags\index.js:108:14
なんのこっちゃ。でも R:node_modules\gulp\index.js
が怪しいですね。
不思議なことに、グローバルに入れた gulp
コマンドをこのディレクトリで叩けば問題なく動きました。
「何故この形式のパスが生まれたのか」「これはrequireできるのか」を探ります。
R:node_modules\gulp\index.js ができるまで
gulpが使っている Liftoff というライブラリは、Node.js本体のモジュール検索処理を真似た resolve というライブラリを使っています。
ディレクトリをさかのぼって検索パスを構築する処理に問題があるのですが、まあまずはコードを見てみましょう。
https://github.com/substack/node-resolve/blob/v1.2.0/lib/node-modules-paths.js
var splitRe = process.platform === 'win32' ? /[\/\\]/ : /\/+/; var parts = start.split(splitRe); var dirs = []; for (var i = parts.length - 1; i >= 0; i--) { if (modules.indexOf(parts[i]) !== -1) continue; dirs = dirs.concat(modules.map(function(module_dir) { return prefix + path.join( path.join.apply(path, parts.slice(0, i + 1)), module_dir ); })); } if (process.platform === 'win32'){ dirs[dirs.length-1] = dirs[dirs.length-1].replace(":", ":\\"); }
区切り文字 \
or /
で分割しておき、そのうちn個を連結して node_modules
を連結する、というのを繰り返しています。R:\hoge
を分割すると R:
と hoge
になるわけですが、path.join()
を使うことで区切り文字を挟んでくれるので安心ですね。
ところで、次の二つの結果は同じでしょうか?
path.join(path.join('R:', 'hoge'), 'node_modules')
path.join('R:', 'hoge', 'node_modules')
どちらも R:\hoge\node_modules
になります。では次は?
path.join(path.join('R:'), 'node_modules')
path.join('R:', 'node_modules')
上は R:node_modules
、下は R:\node_modules
になります。
path.join('R:')
の結果は R:.
という見慣れないものになり、これに何かを連結すると R:node_modules
といった形になります。だったら path.join('R:', 'hoge')
もそうすべきだと思うんですが、こっちは利便性を優先したんですかねえ。
最後の要素だけ修正するコードが書かれていますが、今回は ["R:node_modules", "R:node_modules"]
と要素が重複してるので解決しません。重複してることが問題か。
そうして R:node_modules
という検索パスが生まれました。このあと検索パスから gulp/index.js
を探すわけですが、今回の場合カレントディレクトリが R:\
なので、fs.stat()
や fs.readFile()
に渡しても R:\node_modules\gulp\index.js
と解釈され、特に問題は起きません。
あとは対象モジュールのpackage.jsonにmainが書かれていれば path.resolve()
が呼ばれて(この場で)絶対パスになるようですが、gulpのpackage.jsonには書いてないので、resolve 結果は相対パスのままです。
R:node_modules\gulp\index.js はrequireできるのか
冒頭に書いた通りこれは相対パスなので、./
や ../
で始まっていない相対パスを渡すと node_modules 内で検索されるという話と基本的には同じです。場合によってはカレントディレクトリやルートディレクトリになるという点を除いて。
モジュールの検索にあたっては、現在のモジュールが置かれているディレクトリから順にさかのぼっていき*1、それぞれの node_modules
ディレクトリ(が存在する場合に限る)を基準に path.resolve()
が行われます。ちなみに node_modules\node_modules
は省略されます。
例えば R:\node_modules\gulp\bin\gulp.js
なら、
存在しないR:\node_modules\gulp\bin\node_modules
存在しないR:\node_modules\gulp\node_modules
node_modulesの入れ子は省略R:\node_modules\node_modules
R:\node_modules
とまあ候補は最後のひとつしかないんですが、同じドライブなので相対パス扱いになり、R:\node_modules
+ R:node_modules\gulp\index.js
= R:\node_modules\node_modules\gulp\index.js
は存在しないので失敗します。
では、グローバルに入れた gulp コマンド経由では成功したのは何故でしょうか。
gulp コマンドを使った場合は、gulp コマンドの場所が起点になります。
C:\Users\unarist\AppData\Roaming\npm\node_modules\gulp\bin\node_modules
C:\Users\unarist\AppData\Roaming\npm\node_modules\gulp\node_modules
C:\Users\unarist\AppData\Roaming\npm\node_modules\node_modules
C:\Users\unarist\AppData\Roaming\npm\node_modules
C:\Users\unarist\AppData\Roaming\node_modules
C:\Users\unarist\AppData\node_modules
C:\Users\unarist\node_modules
C:\Users\node_modules
C:\node_modules
候補が2つ残りましたが、上から行きましょう。というかどっちでもいいんです。
パスの解決を行いますが、C:\...
は別ドライブなので相対パスの基点として連結することはできません。次に process.cwd()
を確認すると R:\
なので、これを使って R:\
+ R:node_modules\gulp\index.js
= R:\node_modules\gulp\index.js
となります。
おっと、正解のパスにたどり着いてしまいました。
まとめ
require("R:node_modules\\...")
と書いても、
のいずれかを満たせば、R:\node_modules\...
からロードすることが可能です。
この手の相対パスは意図せずして .\
や \
から読み込まれる可能性があると言った方がいいかもしれない。例のresolveライブラリにちょろっと入っていた修正もそれが原因っぽいし*2。ちなみに今回のresolveライブラリのバグは先月末に修正がマージされているのでそのうちリリースされるはず。
おまけ
- Module._load
- Module._resolveFilename: ロード・キャッシュに使うファイルパスを確定する
- Module._resolveLookupPaths: 明示的な相対パスの判定・検索パスの確定
- Module._findPath: 実際の検索
- キャッシュの確認
- new Module()
- Module.prototype.load: モジュールの読み込み
- Module._nodeModulePaths: そのモジュールを起点とした検索パスを用意しておく
- Module._extensions[ext]: 拡張子ごとのロード処理
- Module._resolveFilename: ロード・キャッシュに使うファイルパスを確定する