2021/07/31

package.json周りを理解する。

@ 酒井悠宇

npmでパッケージをインストールすると自動的に作成されるpackage.json。
パッケージの名前とか、runscriptとか色々なことが書いてあって、なんとなくパッケージを管理している何かなんだろうなと言うのは感じてるけど、package.lock.jsonとかとの関係がイマイチなのと、dependenciesとかdevdependenciesとかもよくわかってないので、
https://qiita.com/righteous/items/e5448cb2e7e11ab7d477#npmcli
とか見ながらその辺をいい感じに理解していきたいと思います。

Nodeとかnpmとかについては
nodebrewでNode.jsを色々してみる
npmとnpxの違いについて調べる
とかに書いておりやす。

前提

Node.jsのプロジェクトはnpmのパッケージ1個に相当する。
「npmのパッケージを作る」と言うのは、「Node.jsのプロジェクトを1つ開発する」のと同じ。
npmで言うパッケージとは、「package.jsonというファイルの親ディレクトリに含まれるファイル群」。
例えば、~/projects/my-project/に package.jsonがあれば、~/projects/my-project/ がそのプロジェクトの一番根っこのディレクトリ(ルートディレクトリ)になる。なのでnpmでは、この下にあるディレクトリ/ファイルを全部をまとめて「パッケージ」と呼んでいると言うこと。

package.jsonを実際に見てみる

世の中のパッケージのpackage.jsonの構造はこんな感じ

{
  "name": "my-package",
  "description": "my first package ever",
  "license": "MIT",
  "version": "1.0.0",
  "bin": "./cli.js",
  "main": "index.js",
  "scripts": {
    "start": "node index.js"
  },
  "dependencies": {
    "axios": "^0.18.0"
  },
  "devDependencies": {
    "eslint": "^5.14.1"
  }
}

重要になってくるのが、bin, main, dependencies, devDependencies, scripts これらの項目みたい。

dependencies & devDependencies

dependencies, 日本語に訳すと「依存関係」。
この項目には、そのパッケージが依存する別のパッケージの名前とバージョンが記述されている。
dependenciesとdevDependenciesの違いは、前者は「実行に必要なパッケージ」。後者は「開発やテストにのみ必要なパッケージ」だ。
機能的な違いとしては、とあるパッケージAをdependenciesとしてインストールするときに、デフォルトではAのdependenciesはインストールされが、AのdevDependenciesはインストールされない。

package.jsonではdependenciesが以下のようにして表される。

 "dependencies": {
   "express": "^4.17.1",
   "request": "~2.88.0"
 },

バージョンの先頭には ^(キャレット) や ~(チルダ) がついていることが多い。
バージョンは . で区切られた3つの数字から構成されている。
これはSemver(Semantic Versioning)という規則に則っている。
この規則では、それぞれの数字が意味を持っている。
一番左の数字は「Major」と呼ばれ、APIの変更を表している。ここが変わると、自分のコードを修正する必要がある。
真ん中の数字は「Minor」と呼ばれ、新機能の追加を表している。自分のコードはおそらく変更する必要はない。
一番右の数字は「Patch」と呼ばれ、バグの修正を表している。自分のコードを変更する必要はない。

dependenciesまたはdevDependenciesでバージョン指定する際、^(キャレット)をつけると、「Mejorは一致し、MinorとPatchは指定されたもの以上」という意味になり、~(チルダ)をつけると、「MejorとMinorは一致し、Patchは指定されたもの以上」という意味になる。^も~もなければ、ちょうどそのバージョンにのみマッチするという意味になる。

scripts

scriptsは簡単に言えばコマンドのエイリアス(別名)である。
例えばcreate-react-appで作成したプロジェクトには

  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },

このように記述されている。
ここに記載されているscriptは、npm run <name>で実行することができる。
上記の例で言うと、npm run startを実行すると、実際にはreact-script startというコマンドが実行されるということ。

main

mainはそのパッケージを外からインポートするときに、どのjavascriptファイルが入り口であるかを指定するものである。
これについてはあまり意味がわからないので色々してみる。

create-react-appで作成したプロジェクトにreactというパッケージ(useStateとかuseEffectとかをインポートするあれ)があったので、とりあえずpackage.jsonを拝見させていただくと、mainには以下のように記述されていた。

"main": "index.js"

なのでindex.jsを見にいってみると、以下のように書いてあった。

'use strict';


if (process.env.NODE_ENV === 'production') {
  module.exports = require('./cjs/react.production.min.js');
} else {
  module.exports = require('./cjs/react.development.js');
}

何らかのファイルをモジュールとしてexportしているので、とりあえず見にいってみる。
<react.production.min.js>

<react.development.js>

見に来たことを後悔するコードが書いてある。
ファイル検索するとuseStateとかuseEffectとかの文字があるので、その辺のロジックとかが書いてあると思われる。
このファイルをreactパッケージの中のindex.jsでmodule.exporsしていて、かつ、index.jsをpackage.jsonのmainに指定しているから、reactのパッケージをインストールしたプロジェクトで、import React from 'react'; みたいにすると、useStateとかuseEffectとかが使えるようになるって感じなのか。
「mainはそのパッケージを外からインポートするときに、どのjavascriptファイルが入り口であるかを指定するもの」
なるほど。

bin

これもパッケージを外から使うときのみ重要になる項目である。
パッケージAのpackage.jsonのbinに何らかの実行可能ファイルが指定されていると、パッケージAをインストールすればそれをCLI(コマンドラインインターフェース)でコマンドとして実行することができるようになる。

うーん。よくわからんので色々見てみる。
create-react-appで作成した自分のプロジェクトのnode_modulesにあるパッケージのpackage.jsonを色々見てみると、react-scriptsというパッケージにbinが指定されてあった。以下のように記述されている。

  "bin": {
    "react-scripts": "./bin/react-scripts.js"
  },

./bin/react-scripts.jsを実行するコマンドは"react-scripts"ですよ。みたいな感じか。
自分のプロジェクトのpackage.jsonのrun-scriptsを見てみると、

  "scripts": {
    "eject": "react-scripts eject"
  },

あった、react-scripts。
ということは、create-react-appで作成した自分のプロジェクトでnpm run ejectを実行するとrun-scriptsのejectに指定してあるreact-scripts ejectが実行される。
ということは、react-scriptsのpackage.jsonのbinに指定されてある"./bin/react-scripts.js"が「./bin/react-scripts.js eject」みたいな感じで実行されるって感じか。binを使うとコマンドをパッケージを跨いで渡せるみたいなイメージ。
ちなみにnpm run ejectコマンドについて調べると、react-scriptsでまとめられている環境を解放して、カスタマイズできるようにするためのコマンドみたい。https://qiita.com/naohikowatanabe/items/71a8bf477216ef56a5b7#npm-run-eject

dependencies, devDependenciesの編集

dependenciesの追加

$ npm install <package>

devDependenciesの追加

$ npm install -D <package>

バージョンを指定してインストール。@の後にバージョンを指定する

npm install react@16.8.6

最新バージョンをインストール

npm install react@latest

dependencies, devDependenciesの削除

npm uninstall <package>

package-lock.json

package-lock.jsonはnpm実行時に自動的に作成されるファイルで、npm installの実行時に自動的に作成される。
dependenciesやdevDepencdenciesのバージョンをロックするという役割がある。

プロジェクトというのは多くの場合複数人で進められるので、開発者全ての環境で全く同じバージョンのdependenciesとdevDependenciesをインストールしたいと思うのは自然なことだ。
なぜならバージョンが異なると「ある環境ではうまくいき、ある環境ではエラーが出る」といったことが起こるからだ。
この全ての環境でバージョンを揃えるというのは、package.jsonだけでは実現することができない。
キャレット^やチルダー~を使わなければいいのではないかと思うかもしれないが、それでも実現することはできない。
なぜなら、自分のpackage.jsonで"A": "1.2.0"としても、パッケージAはパッケージBに依存しており、パッケージBでは、"B": "^2.5.0"と記述されてあるからだ。パッケージをインストールすればするほどこの依存関係は増えていくので、これらのパッケージのバージョンを手作業で固定してくのはあまりに非効率だ。

これを解決するためにpackage-lock.jsonが導入された。
ここには、package.jsonを元にインストールしたdependenciesとdevDependenciesのバージョンが、
パッケージ、パッケージと依存関係のあるパッケージ.....というふうに間接的なものも含めて全て記録されている。

しかしpackage-lock.jsonがあってしても、開発者全員の環境を全く同じにすることができない場合があるみたい。
(node.jsのバージョンの違いや、npmのバージョンの違いがインストールするパッケージのバージョンに違いを生むらしい)
これについてはNodeのバージョン管理みたいな話になるので、今は無視して今後調べていきます。

npm install

パッケージ名なし

パッケージ名を指定せずにnpm install を実行すると、package.jsonとpackage-lock.jsonに基づいてdependenciesとdevDependenciesが全てローカルにインストールされる。
試しにやってみる。何も入っていないディレクトリに、create-react-appで作成したプロジェクトのpackage.jsonとpackage-lock.jsonをコピペ。

パッケージ名なしでnpm installを実行すると

$ npm install

node_modulesが作成された!

githubでソースコードをコピペしてきたけどまだパッケージがない。みたいなときに使えそう。
あとは間違ってnode_modules内のファイルをいじってしまった時とか。

パッケージ名あり

パッケージ名を指定すれば、そのパッケージがローカルにインストールされる。
試してみる。

$ npm init -y

新しくpackage.jsonを作成。

$ npm install cowsay

パッケージをインストール

package-lock.jsonとnode_modulesが追加された!
新しくdependenciesやdevDependenciesをインストールしたい時に使う感じ。

まとめ

package.jsonのdependenciesに記述されているのは、「そのパッケージの実行に必要なパッケージ」
devDependenciesに記述されているのは、「そのパッケージの開発やテストにのみ必要なパッケージ」
バージョンの表現は大体Semver(Semantic Versioning)という規則に則っていて、Major, Minor, Patchと分かれている。
それぞれ、APIの変更、新機能の追加、バグの修正を表している。
バージョンの前に^キャレットをつけると「Mejorは一致し、MinorとPatchは指定されたもの以上」という意味になり、
~チルダをつけると、「MejorとMinorは一致し、Patchは指定されたもの以上」という意味になる。

package.jsonのscriptsに記述されているのは、コマンドのエイリアス(別名)

package.jsonのmainに記述されているのは、そのパッケージを外からインポートする際に、どのjavascriptファイルが入り口であるかを指定するもの。

パッケージAのpackage.jsonのbinに何らかの実行ファイルが記述されてあると、パッケージAをインストールすれば、それをCLIでコマンドとして実行することができる。(コマンドを渡せる的な?)

package-lock.jsonには、プロジェクトに実際にインストールしたパッケージのバージョンが依存関係のあるものも含めて全て記述されている。
開発者の開発環境を揃えるときに役立つ。

package.json周りのディレクトリの関係性を買い物に例えると、
package.jsonは買うものリストで、実際に買ったものはnode_modulesに格納されている。そしてpackage-lock.jsonは領収書みたいなもの。

最後に

まだまだ勉強しないといけないことだらけですが、いい感じにpackage.json周りの知識がついたと思います。
今後もこんな感じで記事書いていきたいでーす!
最後までご覧いただきありがとうございました!

© 2021 powerd by UnReact