package.json は、単なる依存パッケージ一覧ではありません。
プロジェクトの実行方法、利用するツール、対応するNode.js、モジュール形式、公開範囲まで表す「プロジェクトの契約書」です。
この記事では、すべての項目を辞書のように列挙するのではなく、既存プロジェクトを読む順番に沿って整理します。
まず見るべき全体像
{
"name": "example-app",
"private": true,
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"test": "vitest run"
},
"dependencies": {
"react": "^19.0.0"
},
"devDependencies": {
"typescript": "^5.9.0",
"vite": "^7.0.0"
},
"engines": {
"node": ">=22 <26"
},
"packageManager": "pnpm@10.0.0",
"type": "module"
}
初めて見るリポジトリでは、次の順に確認します。
-
scripts:どう起動・テスト・ビルドするか -
dependencies:アプリの主要技術は何か -
devDependencies:開発・テスト・ビルド環境は何か -
enginesとpackageManager:どの環境を想定しているか -
typeとexports:モジュールをどう扱うか -
overridesとworkspaces:依存関係に特別な事情があるか
name・version・private
name
name はパッケージ名です。npmへ公開するパッケージでは一意な名前が必要です。
アプリケーションでも、ログ、ワークスペース、ツールの表示名として使われることがあります。
version
version は通常、Semantic Versioningの major.minor.patch 形式で書きます。
{
"version": "2.4.1"
}
公開しないアプリでは、この値をリリース番号として運用するかどうかはチーム次第です。自動的にアプリのバージョンになるわけではありません。
private
{
"private": true
}
private: true は、誤ってnpmへ公開することを防ぎます。社内アプリやフロントエンドアプリでは、意図がない限り付けておく方が安全です。
「非公開npmパッケージを作る」という意味ではありません。
scriptsは開発フローの入口
scripts には、パッケージマネージャーから実行するコマンドを定義します。
{
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"test": "vitest run",
"test:watch": "vitest",
"lint": "eslint .",
"typecheck": "tsc --noEmit"
}
}
この例から、次のことが読み取れます。
- 開発サーバーはVite
- ビルド前にTypeScriptを確認する
- テストはVitest
- lintと型チェックを別々に実行できる
スクリプト名だけではなく、CIがどのスクリプトを実行しているかも確認します。test が存在しても、CIから呼ばれていなければ品質ゲートにはなっていません。
pre・postスクリプト
pre<名前> と post<名前> は、対応するスクリプトの前後に実行されます。
{
"scripts": {
"prebuild": "node scripts/check-env.js",
"build": "vite build",
"postbuild": "node scripts/report-size.js"
}
}
npm run build では、prebuild → build → postbuild の順に進みます。
暗黙の処理が増えると追跡しづらいため、重要な処理はCI設定と合わせて確認します。
dependenciesとdevDependencies
| 項目 | 基本的な判断 |
|---|---|
dependencies |
パッケージ利用時・アプリ実行時に必要 |
devDependencies |
開発、テスト、ビルド、静的解析に必要 |
たとえば、WebアプリではReactを dependencies、TypeScriptやテストランナーを devDependencies に置くのが一般的です。
{
"dependencies": {
"react": "^19.0.0"
},
"devDependencies": {
"typescript": "^5.9.0",
"vitest": "^3.0.0"
}
}
ただし「ブラウザーで直接使うか」だけでは判断できません。ビルド後にコードへ含まれる依存、Node.js上で実行時に読み込む依存、利用者側に要求する依存では意味が異なります。
デプロイ環境で devDependencies を省くなら、ビルドをどこで行うかも重要です。デプロイ先でビルドする構成では、ビルドツールもその時点では必要です。
peerDependencies
peerDependencies は、主にライブラリやプラグインが「利用側に一緒に存在してほしいパッケージと対応範囲」を表すために使います。
{
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0"
}
}
UIライブラリがReactを自分の依存として別コピーで抱えるのではなく、利用側のReactと組み合わせて動くことを表せます。
peerDependenciesMeta では、peer dependencyをoptionalとして宣言できます。
{
"peerDependencies": {
"@types/react": "^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
}
アプリケーションの通常の依存を、理由なくpeer dependencyへ移すものではありません。
optionalDependencies
optionalDependencies は、インストールに失敗してもパッケージ全体のインストールを継続できる依存関係です。
{
"optionalDependencies": {
"platform-specific-accelerator": "^2.0.0"
}
}
特定OSだけで使う高速化モジュールや、利用できない場合にJavaScript実装へfallbackできる機能などで使われます。
「実行時になくても必ず問題ない」ことをアプリ側で扱う必要があります。
let accelerator;
try {
accelerator = await import("platform-specific-accelerator");
} catch {
accelerator = await import("./javascript-fallback.js");
}
必須処理に必要な依存をoptionalへ移すと、インストールは成功しても実行時に初めて失敗します。失敗を許容できる理由とfallbackをセットで設計します。
bundledDependencies
bundleDependencies または bundledDependencies は、公開するtarballへ特定の依存を同梱するための設定です。
通常のアプリ開発で使う機会は多くありません。オフライン配布など明確な理由がある場合に、パッケージ容量、ライセンス、更新方法を確認して使います。
バージョン範囲とlock file
{
"dependencies": {
"example": "^2.4.1"
}
}
-
2.4.1:そのバージョンだけ -
~2.4.1:通常はpatch更新を許可 -
^2.4.1:通常はminorとpatch更新を許可
0系では互換性の扱いが異なるため、^0.x.y を「常にminorまで上がる」と覚えない方が安全です。
package.json が許可範囲を表し、lock fileが実際に解決した依存ツリーを記録します。アプリケーションでは、利用しているパッケージマネージャーのlock fileを通常コミットします。
複数種類のlock fileが混在している場合は、どのパッケージマネージャーを正とするか確認します。
enginesとpackageManager
engines は、想定するNode.jsやnpmのバージョン範囲を表します。
{
"engines": {
"node": ">=22 <26"
}
}
環境や設定によっては警告にとどまるため、engines だけで実行環境を完全に固定できるとは限りません。CI、コンテナ、バージョン管理ツールの設定とも揃えます。
packageManager は、想定するパッケージマネージャーとバージョンを示します。
{
"packageManager": "pnpm@10.0.0"
}
Corepackはこの項目を利用できますが、Node.js 25以降はNode.js本体へ同梱されません。Corepackを前提にする場合は、導入方法もREADMEやCIへ明記します。
type・main・exports
type
{
"type": "module"
}
Node.jsでは、type: "module" を指定したパッケージ内の .js をES Modulesとして扱います。type: "commonjs" または項目を省略した場合との違いを理解しておく必要があります。
.mjs と .cjs は、パッケージの type より明示的にモジュール形式を指定します。
main
main は、従来から使われてきたパッケージの主要なエントリーポイントです。
{
"main": "./dist/index.js"
}
アプリケーションより、公開ライブラリで重要になります。
exports
exports は、利用者へ公開する入口を明示し、内部ファイルへのアクセスを制限できます。
{
"exports": {
".": "./dist/index.js",
"./react": "./dist/react.js"
}
}
exports を追加すると、それまで利用者が直接importしていた内部パスを使えなくする可能性があります。既存ライブラリへ導入するときは、破壊的変更にならないか確認します。
import と require で別ファイルを公開する条件付きexportsもありますが、型定義と二重パッケージ問題を含めて設計が必要です。単純なアプリでは、理解せずに追加する項目ではありません。
imports
imports は、そのパッケージ内部で使うimport aliasを定義します。キーは # から始めます。
{
"imports": {
"#config": "./src/config.js",
"#internal/*": "./src/internal/*.js"
}
}
import { config } from "#config";
外部利用者へ公開する exports と、パッケージ内部の参照を定義する imports を混同しないようにします。
TypeScriptの paths やバンドラー独自aliasも似た用途を持ちますが、実行時のNode.jsが同じ設定を読むとは限りません。型チェック、テスト、ビルド、実行のすべてで同じspecifierを解決できるか確認します。
型定義を公開する
TypeScript向けライブラリでは、JavaScriptの入口だけでなく型定義の入口も必要です。
{
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
}
}
対応するTypeScriptやNode.jsのバージョン、ESMとCommonJSの配布方法によって推奨構成は変わります。
公開前には、別の小さなプロジェクトから実際にインストールし、次を確認します。
- package rootからimportできる
- 公開したsubpathからimportできる
- 非公開の内部パスへ依存していない
- 型定義が正しく選ばれる
- Node.jsと対象バンドラーで読み込める
files・bin・browser
公開パッケージでは、次の項目も使われます。
| 項目 | 目的 |
|---|---|
files |
npmパッケージへ含めるファイルを絞る |
bin |
CLIコマンドを公開する |
browser |
一部のバンドラーへブラウザー向け入口や置換を伝える |
browser はNode.jsの標準モジュール解決項目ではなく、対応するツール側の規約です。どのバンドラーを対象にするか確認します。
公開前は npm pack --dry-run で、実際に含まれるファイルを確認すると安全です。
overrides
overrides は、依存ツリー内のパッケージバージョンを上書きするnpmの機能です。
{
"overrides": {
"some-library": {
"problematic-package": "3.2.1"
}
}
}
脆弱性や既知バグへ一時対応するときに役立ちますが、依存元が検証していない組み合わせを強制する可能性があります。
追加するときは、次を残します。
- なぜ必要なのか
- どのissueや脆弱性に対応したのか
- いつ削除できるのか
- lock fileとテスト結果
pnpmやYarnでは、同じ目的でも設定場所や機能名が異なります。npmの例をそのままコピーしないようにします。
workspaces
workspacesは、複数パッケージを1つのリポジトリで管理するときに使います。
{
"private": true,
"workspaces": [
"apps/*",
"packages/*"
]
}
ルートの package.json は全体のツールや共通スクリプト、各パッケージの package.json はそれぞれの依存と公開設定を持つ、という分担が一般的です。
パッケージ間依存の記法や解決方法は、npm・pnpm・Yarnで差があります。
workspaceでは、ルートと各packageの責務を分けて読みます。
repository/
├─ package.json
├─ apps/
│ └─ web/package.json
└─ packages/
└─ ui/package.json
- ルート:全体で使うpackage manager、共通script、workspace定義
- app:実行時依存とアプリ固有script
- library:peer dependency、exports、公開ファイル
依存をすべてルートへ集めると、各packageが本当に必要とする依存が分からなくなります。package単体でbuild・test・publishできるかを基準に配置します。
package.jsonから依存理由を調べる
dependencies に名前がないpackageが、lock fileや node_modules に存在することがあります。これは多くの場合、別packageが必要とする推移的依存です。
npm explain package-name
npm ls package-name
npm explain は、どの依存経路から入ったかを確認するときに役立ちます。
example-app
└─ build-tool
└─ package-name
直接更新できるのか、上位packageを更新すべきか、overrides が必要なのかを判断する前に依存経路を確認します。
npm pkg get を使うと、scriptなど特定項目を機械的に取得できます。
npm pkg get scripts
npm pkg get engines
CIで独自にJSONを正規表現処理するより、JSON parserやpackage managerのコマンドを利用します。
scriptsを信頼境界として考える
packageのinstall時には、preinstall、install、postinstall などのlifecycle scriptが実行される場合があります。
これはnative addonのbuildなどに必要ですが、依存packageのコードが開発端末やCIで実行される境界でもあります。
新しい依存を追加するときは、名前とバージョンだけでなく次を確認します。
- 公式な配布元か
- install scriptを持つか
- maintainerや所有者が大きく変わっていないか
- lock fileに意図しないregistry URLがないか
- CIへ渡しているtokenへ不要にアクセスできないか
npm install --ignore-scripts はscript実行を抑止できますが、scriptを必要とするpackageは正常に使えなくなる場合があります。常に付ければ安全というものではなく、プロジェクトの依存とCI方針に合わせます。
また、scripts のコマンドはshellを経由します。OSごとの差、環境変数の展開、引数のquoteを考慮し、外部入力をそのままshell commandへ連結しません。
実務で読むときのチェックリスト
- READMEが案内するコマンドと
scriptsが一致しているか - CIがtest・lint・typecheck・buildのどれを実行しているか
- 使用するNode.jsとパッケージマネージャーが明示されているか
- lock fileが1種類に統一されているか
-
typeとコードのimport形式が一致しているか -
overridesに理由と終了条件があるか - 公開パッケージなら
exports、files、型定義を検証しているか - 使われていない依存やスクリプトが残っていないか
まとめ
package.json は、項目名を暗記するより、次の3つに分けて読むと理解しやすくなります。
- 開発フロー:
scripts - 依存と環境:dependencies系、
engines、packageManager - 配布方法:
type、exports、files、bin
特別な設定を見つけたら、「何を可能にしているか」だけでなく「なぜ必要になったか」まで確認することが、保守しやすいプロジェクトにつながります。