🆚

VSCode の Jest 拡張機能が asdf で管理している Node.js を呼び出せない問題

2023/06/10に公開

VSCode の Jest 拡張機能(vscode-jest)がたまに asdf で管理している Node.js を呼び出せない問題に遭遇しました。
今回は、その原因と対策についてちょっと深掘りしたので、ここに残します。

TL;DR

  • VSCode の Jest 拡張機能は非インタラクティブ(non-interactive)&非ログイン(non-login)な /bin/sh シェルから Node.js を呼び出す
  • asdf は多くの場合 ~/.bashrc~/.zshrc に asdf の初期化コード(パスを通したり)を記述しているため、asdf で管理している Node.js を Jest 拡張機能は呼び出せない
  • いくつかの対策方法があるが、.vscode/settings.jsonjest.shell で Jest 実行のシェルをログインシェルにするのがたぶん楽。たぶん
    .vscode/settings.json
    "jest.shell": {
      "path": "/usr/bin/env",
      "args": ["zsh", "--login"]
    },
    

VSCode の Jest 拡張機能 vscode-jest について

VSCode には Jest[1]のテストを実行するための便利な拡張機能があります。

https://marketplace.visualstudio.com/items?itemName=Orta.vscode-jest

名を vscode-jest と言い、VSCode 上から Jest のテストを実行したり、テストの実行結果を見たりできます。

何があったか

ある日、VSCode で Node.js を使うプロジェクトを開いたところ、vscode-jest がエラーを吐きました。

> Test run exited at 2023/6/7 18:31:05 <

env: node: No such file or directory

[error] failed to retrieve test file list. TestExplorer might show incomplete test items

[info] jest process failed to start, most likely due to env or project configuration issues, please see: https://github.com/jest-community/vscode-jest/blob/master/README.md#jest-failed-to-run

env: node: No such file or directory

[error] Jest process "watch-tests" ended unexpectedly

[info] jest process failed to start, most likely due to env or project configuration issues, please see: https://github.com/jest-community/vscode-jest/blob/master/README.md#jest-failed-to-run

テストファイルの一覧が取得できないエラーと、ファイルを監視して自動でテストを動かす watch-tests が失敗したというエラーの 2 つが出ていますが、どちらのエラーでも env: node: No such file or directory が出ており、おそらく原因は同じと考えられます。

不思議だったのが、これまでは特段設定をしなくても Jest の拡張機能でテスト実行ができていたことです。
最初は何がいつもと違うのかわかりませんでした。

起こる条件

何回か色々試したところ、上記のエラーが出るのは code コマンドを使ってない時でした。

code コマンドを使わずに VSCode を起動するとは、例えば Dock の VSCode を右クリックして出てくる最近開いたディレクトリ一覧から開いたり、メニューバーの最近開いたディレクトリ一覧から開いたりすることを指しています。


Dock から開く場合


メニューバーから開く場合

実際、僕は Dock から VSCode を開くことがしばしばあり、その時にエラーが出ることがわかりました。

原因

色々ググったりして調べていたところ、個人の有志が書いた次のページが見つかりました。

https://scrapbox.io/pnly/RESOLVED%3A_vscode-jest_`env:_node:_No_such_file_or_directory`

このページに書いてあることと、vscode-jest の Issue コメントが全てなのですが、噛み砕いて行きます。

https://github.com/jest-community/vscode-jest/issues/741#issuecomment-921222851

vscode-jest は ~/.bashrc などを読まない

どうやら、vscode-jest は Jest を呼び出す際に、非インタラクティブ(non-interactive)&非ログイン(non-login)な /bin/sh シェルから Node.js、ひいては Jest を呼び出しているようです。

したがって、vscode-jest は ~/.bashrc のようなファイルで export した環境変数を読んでくれないわけですね。

non-login な /bin/sh はどこへのパスが通っている?

そもそも non-login な /bin/sh はどこへのパスが通っているのでしょうか?

まずは env -i /bin/sh -c 'env' で環境変数を確認してみます。

非インタラクティブ&非ログインなシェルの環境変数一覧
❯ env -i /bin/sh -c 'env'
PWD=<実行ディレクトリのパス>
SHLVL=1
_=/usr/bin/env

おっと、$PATH がないですね。$PATH は定義されているのでしょうか?

env -i /bin/sh -c 'echo $PATH' で確認できます。

非インタラクティブ&非ログインなシェルの $PATH
❯ env -i /bin/sh -c 'echo $PATH'
/usr/gnu/bin:/usr/local/bin:/bin:/usr/bin:.

envPATH が定義されてなかったのに、$PATH の中身が表示されました。
これは、PATH がデフォルトのシェル変数として登録されてるからですね[2]

man bash に載っています。

man bash からの抜粋
PATH   The search path for commands.  It is a colon-separated list of directories in which the shell looks for commands (see COMMAND EXECUTION below).  A zero-length (null) directory name in the value
      of PATH indicates the current directory.  A null directory name may appear as two adjacent colons, or as an initial or trailing colon.  The default path is system-dependent, and is set by the
      administrator who installs bash.  A common value is ``/usr/gnu/bin:/usr/local/bin:/usr/ucb:/bin:/usr/bin''.

The default path is system-dependent, and is set by the administrator who installs bash. A common value is ``/usr/gnu/bin:/usr/local/bin:/usr/ucb:/bin:/usr/bin''.

日本語訳)デフォルトのパスはシステムに依存し、bashをインストールした管理者が設定します。 一般的な値は ``/usr/gnu/bin:/usr/local/bin:/usr/ucb:/bin:/usr/bin'' です。

どうやらシステム依存(インストールした人が決める)らしいです。
macOS (darwin?) の場合はそれらの開発者が設定してるのかもしれません。

少なくとも僕の環境の /bin/sh では、次の場所が指定されていました。

  • /usr/gnu/bin
  • /usr/local/bin
  • /bin
  • /usr/bin
  • .

したがって、これらの場所に Node.js のバイナリ(node)がないといけないわけですね。

自分の環境では non-login な /bin/sh において Node.js へのパスが通ってなかった

例えば、Node.js を Homebrew でインストールする際は、最終的に /opt/homebrew/bin/ に配置されます。
通常 /opt/homebrew はパスが通ってない($PATH に含まれていない)ので、/opt/homebrew/bin/brew shellenv~/.bashrc などで実行するようにして、/opt/homebrew/bin/ へのパスを通す必要があります[3]

そして僕の場合、Node.js を asdf[4] で管理しています。

❯ where node
/Users/<ユーザ名>/.asdf/shims/node
/opt/homebrew/bin/node  # 注釈:Homebrew でも Node.js が入っていた

asdf で管理するバイナリは ~/.asdf/shims に配置されるため、Homebrew と同じく、パスを通す必要があります。
パスは ~/.zshrc 経由で設定するようにしているため、/bin/sh ではパスが通っていない状態になっていました(.zshrc で設定しているため、login、non-login 関わらずパスが通らない)。

これが原因だったんですね。

解決方法

どのように解決するかについても、上記で紹介した方のページに書かれていました。

https://scrapbox.io/pnly/RESOLVED%3A_vscode-jest_`env:_node:_No_such_file_or_directory`

この著者の場合、jest.shell に普段利用するシェル(Zsh)を設定し、.zshenv でパスを通すことで回避しています。

vscode-jest には jest.shell という設定項目があり、そこで Jest の呼び出しに使うシェルを設定できます(何も設定しなければ /bin/sh)。

また、Zsh の場合、どの場合((non-)login、(non-)interactive)でも ~/.zshenv ファイルを読み込むため[6]~/.zshenv でパスを通すことで、Zsh から Jest を呼び出す際に Node.js へのパスが通るようになります。

しかし、~/.zshenv に手を加えると、Zsh のシェルスクリプト実行時なども影響を受けてしまうため、なるべく書き換えたくないというのがあります。

それを回避する方法として、上記ページには Jest の実行に使うシェルをカスタマイズできる変更が入りそうとのことも書かれていました。当時は未リリースだったようですが、すでにリリースされてそうだったので、今回は --login でログインシェルとして Jest を実行するようにしてみました。

実際の設定

.vscode/settings.json
"jest.shell": {
  "path": "/usr/bin/env",
  "args": ["zsh", "--login"]
},

というわけで、実際の設定が上記のものになります。
僕の場合、Zsh を env 経由で呼び出し、--login オプションで Zsh をログインシェルとして起動するようにしています。

ログインシェルとして起動することにより、~/.zprofile~/.zlogin が読まれるようになります。
そちらで Node.js へのパスを通すようにすることで、~/.zshenv に手を加えずに済みます。

この設定を対象リポジトリの .vscode/settings.json に書き加えることで、冒頭の問題は解決できました。

ただ、場合によっては /bin/sh を起動するときよりも時間がかかるようになるかもしれません。
僕の場合は特に速度面で気になることはありませんでした。

おわりに

今回 VSCode の Jest 拡張の謎の挙動から始まり、ワークアラウンドはすぐに見つかったのですが、なんでそういう挙動になるのかが気になって色々調べました。

とりあえず再発しないようになって良かったです。
シェルのモードの違いとか読み込むファイルの順序とか、なかなか勉強になって良かったですね。

なんか間違ったことを書いてしまっていたり、もっと良い解決方法があったりしたらぜひ教えてください。

脚注
  1. JavaScript のテスティングフレームワーク。https://jestjs.io ↩︎

  2. ここら辺の挙動はシェルによって変わると思います。 ↩︎

  3. Homebrew インストール後にやっといてねって出てくる。https://github.com/Homebrew/install/blob/716a1d024f32890ef75ea82c18a769abc24e9475/install.sh#L1010-L1025 ↩︎

  4. ざっくり言うとバイナリのバージョン管理ツールです。ディレクトリごとに異なるバージョンのバイナリを使えます。 ↩︎

  5. 環境変数が引き継がれる仕組みはおそらく周知の事実だと思いますが、そこらへんの仕様が載ったページを貼りたくて探しました。しかし見つけられなかったので、誰か知ってる人いたら教えてください。 ↩︎

  6. 詳しくは man zshSTARTUP/SHUTDOWN FILES を参照してください。https://linux.die.net/man/1/zsh#STARTUP/SHUTDOWN FILES ↩︎

GitHubで編集を提案

Discussion