package.jsonを1行ずつ解剖

依存関係の事故は理解の穴から起きる。現実的なWebアプリのpackage.jsonを1行ずつ精読し、^と~の差、devDependenciesの境界、npm ciが再現性を守る仕組みまで、実務の判断基準を固められる。

応用設定ファイルnpmJavaScriptNode.js最終更新: 2026-07-28
3つの要点
TL;DR
  1. dependenciesは実行時、devDependenciesは開発時の依存で、npm install --omit=devでは後者を除く。peerDependenciesは利用側と共有する依存を宣言し、npm 7以降は自動導入される。
  2. バージョン指定の^はメジャーを、~はマイナーを固定し、それより下の桁の更新だけを許す。ただし^0.33.4のような0.x系では最初の非ゼロ桁が境界になり、0.34.0以上へは自動更新されない特殊ルールがある。
  3. package.jsonが宣言するのは許容範囲で、package-lock.jsonが全依存の正確なバージョンと整合性ハッシュを記録する。npm ciはロックの内容だけを機械的に再現し、範囲の再解決をしないためCIや本番の環境差が消える。

この設定ファイルは何者か

package.jsonはNode.jsプロジェクトのマニフェストだ。名前とバージョン、依存ライブラリの一覧、開発用コマンド、モジュール形式の宣言を1つのJSONファイルに集約し、npm・yarn・pnpmといったパッケージマネージャーはすべてこのファイルを起点に動く。フロントエンドでもサーバーサイドでも、JavaScript開発はこの1枚を中心に回る。なお仕様上は純粋なJSONであり、コメントを書けないという制約も持つ。

横にスクロール

package.jsonから依存解決とスクリプト実行および再現性を検証する手順
マニフェストの範囲指定とlockfileの確定版を分け、クリーン導入で再現性を確認する。

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とサプライチェーン攻撃

依存パッケージが持つ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.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、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
  • 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。

次に確認する観点

設定ファイルnpmJavaScriptNode.js