package.jsonを1行ずつ解剖
依存関係の事故は理解の穴から起きる。現実的なWebアプリのpackage.jsonを1行ずつ精読し、^と~の差、devDependenciesの境界、npm ciが再現性を守る仕組みまで、実務の判断基準を固められる。
- dependenciesは実行時、devDependenciesは開発時の依存で、npm install --omit=devでは後者を除く。peerDependenciesは利用側と共有する依存を宣言し、npm 7以降は自動導入される。
- バージョン指定の^はメジャーを、~はマイナーを固定し、それより下の桁の更新だけを許す。ただし^0.33.4のような0.x系では最初の非ゼロ桁が境界になり、0.34.0以上へは自動更新されない特殊ルールがある。
- package.jsonが宣言するのは許容範囲で、package-lock.jsonが全依存の正確なバージョンと整合性ハッシュを記録する。npm ciはロックの内容だけを機械的に再現し、範囲の再解決をしないためCIや本番の環境差が消える。
この設定ファイルは何者か
package.jsonはNode.jsプロジェクトのマニフェストだ。名前とバージョン、依存ライブラリの一覧、開発用コマンド、モジュール形式の宣言を1つのJSONファイルに集約し、npm・yarn・pnpmといったパッケージマネージャーはすべてこのファイルを起点に動く。フロントエンドでもサーバーサイドでも、JavaScript開発はこの1枚を中心に回る。なお仕様上は純粋なJSONであり、コメントを書けないという制約も持つ。
横にスクロール
1行ずつ解剖
現実的なWebアプリを想定した、次のpackage.jsonを上から順に読み解いていく。
{
"name": "taskboard-web",
"version": "1.4.2",
"private": true,
"type": "module",
"engines": { "node": ">=20.0.0" },
"main": "./dist/server.js",
"exports": {
".": "./dist/server.js"
},
"scripts": {
"dev": "vite",
"prebuild": "node scripts/check-env.js",
"build": "vite build",
"test": "vitest run",
"start": "node dist/server.js"
},
"dependencies": {
"express": "^4.19.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"sharp": "^0.33.4"
},
"devDependencies": {
"typescript": "~5.5.4",
"vite": "^5.4.0",
"vitest": "^2.0.5"
}
}
name・version・private — パッケージの戸籍
nameとversionの組がパッケージの識別子になる。nameは小文字のURLセーフな文字で構成し、スコープを含めて214文字以内という制約がある。versionはセマンティックバージョニングに従ったMAJOR.MINOR.PATCH形式で、npmレジストリでは一度公開したバージョンを二度と上書きできない。番号の刻み方に意味を持たせる理由はなぜセマンティックバージョニングかで詳しく扱っている。
private: true は「このパッケージは公開しない」という宣言で、これがあると npm publish はエラーで止まる。社内アプリを誤ってレジストリへ公開する事故を1行で防げるため、公開予定のないアプリでは付けておくのが安全だ。
type — ESMとCommonJSの分岐点
typeにmoduleを指定すると、このパッケージ内の.jsファイルはESM(import/export構文)として扱われる。省略時はCommonJS(require/module.exports)扱いだ。拡張子.mjsは常にESM、.cjsは常にCommonJSで、typeの指定より優先される。CommonJS前提の古いライブラリと混在すると読み込みエラーの温床になる、影響範囲の大きい1行だ。
engines — 動作するNodeバージョンの宣言
enginesは要求する実行環境の宣言で、サンプルではNode 20以上を求めている。ただしnpmの既定では条件を満たさなくても警告が出るだけで、止めたければ.npmrcでengine-strict=trueを設定する。強制力は弱くても、ローカルとCIのNode差を早期に可視化できる価値は大きい。
main と exports — エントリポイントの公開範囲
mainは古くからある単一のエントリポイント指定だ。exportsはその後継で、両方あるときはexportsが優先される。役割は2つ。import用とrequire用の出し分けなどサブパスと条件の定義、そしてカプセル化だ。exportsに列挙しなかったファイルはパッケージ外から読み込めなくなる。アプリ本体では影響が小さいが、ライブラリを公開するなら設計の要になる。
scripts — pre/postフックが自動で走る
scriptsは npm run で呼び出すコマンド集だ。重要なのが命名規則によるフックで、pre/postを冠した同名スクリプトがあれば本体の前後で自動実行される。サンプルで npm run build を実行すると、先にprebuild(環境変数の検査)が自動で走る。明示的に呼んでいないコマンドが動くため、知らないとビルドログを読み違える。
依存パッケージが持つpostinstallスクリプトは、npm installの過程で自動実行される。この性質はマルウェアの実行経路として実際に悪用されてきた。npm install --ignore-scriptsで抑止できる。
dependencies・devDependencies・peerDependencies — 3つの依存の境界線
dependenciesは実行時に必要な依存で、どんなインストールでも必ず入る。devDependenciesはビルドツールやテストフレームワークなど開発時にだけ必要なもので、npm install --omit=dev(旧来の--productionに相当)を付けた本番インストールでは除外される。サンプルではvite・vitest・typescriptが該当する。
peerDependenciesはアプリにはあまり登場せず、主にライブラリやプラグインが使う。「自分をインストールする側が持っているはずの依存」を宣言するもので、React用のUIライブラリがreactをここに置き、アプリ側と実体を共有するのが典型だ。npm 6までは手動インストールが必要だったが、npm 7以降は自動でインストールされ、バージョンが矛盾するとERESOLVEエラーになる。
| フィールド | 典型的な中身 | 本番インストールでの扱い |
|---|---|---|
| dependencies | 実行時に必要なライブラリ | 必ず入る |
| devDependencies | ビルド・テスト用ツール | --omit=devで除外される |
| peerDependencies | 利用側と共有する前提の依存 | npm 7以降は自動で入る |
semverレンジ — ^ と ~ の正確な意味
依存のバージョンに付く ^ や ~ は、どこまでの自動更新を許すかの指定だ。^はメジャーを固定してマイナーとパッチの更新を許し、~はマイナーまで固定してパッチの更新だけを許す。
| 指定 | 固定される桁 | 許容範囲 |
|---|---|---|
| ^4.19.2 | メジャー | 4.19.2以上 5.0.0未満 |
| ~5.5.4 | マイナーまで | 5.5.4以上 5.6.0未満 |
| ^0.33.4 | 最初の非ゼロ桁 | 0.33.4以上 0.34.0未満 |
注意すべきは0.x系の特殊ルールだ。^の正確な意味は「最初の非ゼロ桁を変えない範囲」であり、^0.33.4は0.34.0未満までしか許さない。セマンティックバージョニングの仕様がメジャー0を「何が壊れてもよい開発版」と定めているため、npmは0.x系のマイナー更新を事実上の破壊的変更として扱う。
^0.0.3が許すのは0.0.3だけで、0.0.4にすら上がらない。メジャーもマイナーも0のパッケージでは、^は完全な固定と同じ意味になる。
package-lock.json — 範囲の宣言と結果の記録
package.jsonが書くのは許容範囲であって、実際に何が入ったかは記録しない。その役割を担うのがpackage-lock.jsonで、直接依存だけでなくすべての推移的依存について、解決された正確なバージョン・取得元・改ざん検知用の整合性ハッシュを記録する。
この分担を前提に、コマンドは2つある。npm installはpackage.jsonの範囲内で解決をやり直し、必要ならロックファイルを更新する。一方の npm ci はロックファイルを機械的に再現する専用コマンドで、node_modulesを削除してから正確に入れ直し、package.jsonとロックの不整合を検出するとエラーで止まり、ロックファイルを一切書き換えない。CIや本番で npm ci を使う限り、日によってインストール結果が変わる事態は起きない。
つまずきやすい点
まず、^を付けたのに0.x系の依存が更新されない混乱。^は最初の非ゼロ桁を境界にするため、^0.33.4が0.34.0を取り込むことはない。上げたければ範囲を書き換えるか、npm install sharp@latest と明示する。
次に、package-lock.jsonをコミットしない事故。生成物に見えるため.gitignoreへ入れてしまう例が後を絶たないが、ロックがなければCIやチームメイトは範囲から毎回解決をやり直し、人や日によって違うバージョンが入る。npm公式もリポジトリへのコミットを推奨している。
3つ目は npm install と npm ci の使い分け。依存を追加・更新する開発中はinstall、CI・本番・クリーンな再現が欲しい場面はciが原則だ。ciを日常開発に使うと毎回node_modulesが消えて遅く、installをCIに使うとロックが書き換わって再現性が崩れる。
最後に、scriptsのOS差。スクリプトはWindowsではcmd.exe、macOSやLinuxでは/bin/shで実行されるため、環境変数の参照構文や rm -rf のようなUnixコマンドはWindowsで壊れる。cross-envやrimrafといった互換パッケージで吸収するか、ロジックをNodeスクリプトに寄せて npm run から呼ぶのが定石だ。
まとめ
package.jsonは、識別情報からモジュール形式、実行環境、エントリポイント、スクリプト、3種類の依存宣言までを1枚で束ねるNode.jsプロジェクトの中枢だ。そのうえで、package.jsonが範囲を宣言し、package-lock.jsonが結果を記録し、npm ciがそれを再現する3点セットで初めてビルドの再現性が成立する。JavaScriptやNode.jsの基礎はプログラミングから辿れる。
設定ファイル解剖の記事ガイド
package.jsonを1行ずつ解剖を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
設定ファイル
比較で見る軸
難易度: advanced / カテゴリ: 設定ファイル解剖 / タグ数: 4
導入後に効く点
バージョン指定の^はメジャーを、~はマイナーを固定し、それより下の桁の更新だけを許す。ただし^0.33.4のような0.x系では最初の非ゼロ桁が境界になり、0.34.0以上へは自動更新されない特殊ルールがある。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- 設定ファイル解剖
- タグ数
- 4
判断チェックリスト
- 自社の用途が「設定ファイル / npm」に近いか確認する。
- 強みである「dependenciesは実行時、devDependenciesは開発時の依存で、npm install --omit=devでは後者を除く。peerDependenciesは利用側と共有する依存を宣言し、npm 7以降は自動導入される。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。