For

2023.9.16

npm run, yarn, npxでコマンドがどのように実行されるか

概要

npm run <command> or yarn <command>npx <command> を良く実行していると思いますが、それらがどのように実行されているのかをまとめてみます。

準備

今回は適当なディレクトリで yarn create next-app or npx create-next-app を実行してNext.jsのプロジェクトを作成し、確認していきます。
確認だけなので私は /tmp/my-app というディレクトリに作成しました。

詳細を確認していく

package.json

今回作成したプロジェクトの package.json は以下のようになっています。

json_____package.json_____{
  "name": "my-app",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@types/node": "20.6.2",
    "@types/react": "18.2.21",
    "@types/react-dom": "18.2.7",
    "autoprefixer": "10.4.15",
    "eslint": "8.49.0",
    "eslint-config-next": "13.4.19",
    "next": "13.4.19",
    "postcss": "8.4.29",
    "react": "18.2.0",
    "react-dom": "18.2.0",
    "tailwindcss": "3.3.3",
    "typescript": "5.2.2"
  }
}


scriptsを見てみると "dev": "next dev" というスクリプトが定義されています。
scriptsで定義してあるスクリプトは npm run <command> or yarn <command> で実行できます。
この記事は terminalでどのようにコマンドが実行できるのかグローバルにインストールされたyarnで確認する の続きみたいなものなのでこの記事でもyarnで確認していきます。

yarn dev

yarn dev を実行するとpackage.jsonのscriptsで定義した dev が実行されるので next dev が実行されます。
yarn <command> で実行されるコマンドは以下の流れで実行できるか確認されます。

・そのプロジェクトの node_modules/.bin にコマンドが存在するか確認し、存在すれば実行
・上記に存在しない場合は $PATH に定義されたpathのリストからコマンドが存在するか確認し、存在すれば実行、存在しなければエラーで終了

node_modules/.bin

ということで node_modules/.bin を見てみます。
このディレクトリに移動し ls -la します。

bash_____terminal_____ls -la
~~省略~~
lrwxr-xr-x    1 kobayashi  wheel    21  9 16 18:34 next -> ../next/dist/bin/next
~~省略~~


next がありました。
また next はシンボリックリンクになっているのでシンボリックリンク先も見ておきます。

bash_____terminal_____ls -la
drwxr-xr-x   5 kobayashi  wheel   160  9 16 18:34 .
drwxr-xr-x  19 kobayashi  wheel   608  9 16 18:34 ..
-rwxr-xr-x   1 kobayashi  wheel  5554  9  9 22:38 next
-rw-r--r--   1 kobayashi  wheel    31  9  9 22:38 next.d.ts
-rwxr-xr-x   1 kobayashi  wheel  3031  9  9 22:38 next.map


next が確認できました。
中身は以下のようになっています(長いので最初の方だけ貼ります)。

javascript_____next_____#!/usr/bin/env node
"use strict";
Object.defineProperty(exports, "__esModule", {
    value: true
});
const _log = /*#__PURE__*/ _interop_require_wildcard(require("../build/output/log"));
const _index = /*#__PURE__*/ _interop_require_default(require("next/dist/compiled/arg/index.js"));
const _constants = require("../lib/constants");
const _commands = require("../lib/commands");
function _interop_require_default(obj) {
    return obj && obj.__esModule ? obj : {
        default: obj
    };
}
function _getRequireWildcardCache(nodeInterop) {
    if (typeof WeakMap !== "function") return null;
    var cacheBabelInterop = new WeakMap();
    var cacheNodeInterop = new WeakMap();
    return (_getRequireWildcardCache = function(nodeInterop) {
        return nodeInterop ? cacheNodeInterop : cacheBabelInterop;
    })(nodeInterop);
}
function _interop_require_wildcard(obj, nodeInterop) {
    if (!nodeInterop && obj && obj.__esModule) {
        return obj;
    }
// 続く


ということで yarn dev を実行するとこのファイルが実行されることがわかりました。
もっと細かいことを言うとそもそもなんで yarn <command>package.json のscriptsで定義されたコマンドが実行されるのか気になる人は GitHubのyarnリポジトリ からソースを見てみてください。
ちゃんと読まなくてもChatGPTに要約してもらうだけでも見ないよりいいと思います。

npxではどうなるか

yarn <command> について理解できたところで今度はnpxについて触れていきます。
npxもコマンドを実行できますが package.json のscriptsにあるスクリプトを実行する機能はありません。
npxはコマンドを実行するとき package.json に関係なく以下の流れでコマンドの実行を試みます。

・そのプロジェクトの node_modules/.bin にコマンドが存在するか確認し、存在すれば実行
・上記に存在しない場合は $PATH に定義されたpathのリストからコマンドが存在するか確認し、存在すれば実行
・上記に存在しない場合はnpmレジストリに登録されているパッケージを一時的にダウンロードして実行、npmレジストリからも見つからなければ終了

yarn <command> でコマンドを実行したときと比べると3番目が異なります。
そのため、上記の1番目、2番目の順に実行できなければnpmレジストラから対象のパッケージを一時的にダウンロードして実行することになります。

だから npx create-next-app をすると npmレジストリに登録された create-next-app を一時的にダウンロードして実行してくれるという仕組みになっています( create-next-app というパッケージを node_modules/.bin またはグローバルにインストールしている人はいないと思うので)。

まとめ

とりあえず使っている人もある程度いると思うのでまとめてみました。
これで楽しいnpmライフを送っていただければと思います。