package.json
package.json
の "dependencies" プロパティpackage.json
の "bin" プロパティpackage.json
の "license" プロパティnode:
プロトコルインポート本章では、npm パッケージとは何か、そしてそれらが ESM モジュールとどのように相互作用するかを説明します。
必要な知識:ECMAScript モジュールの構文についてある程度理解していることを前提としています。理解していない場合は、「JavaScript for impatient programmers」の「モジュール」の章をお読みください。
JavaScript エコシステムにおいて、パッケージとはソフトウェアプロジェクトを整理する方法です。標準化されたレイアウトを持つディレクトリです。パッケージにはあらゆる種類のファイルを含めることができます。例えば
パッケージは他のパッケージに依存することができます(これは依存関係と呼ばれます)。それらには以下が含まれます。
パッケージの依存関係は、そのパッケージ内にインストールされます(すぐに方法を説明します)。
パッケージ間の一般的な違いの1つは、
次の小節では、パッケージを公開する方法について説明します。
パッケージを公開する主な方法は、オンラインソフトウェアリポジトリであるパッケージレジストリにアップロードすることです。事実上の標準はnpm レジストリですが、唯一の選択肢ではありません。例えば、企業は独自の内部レジストリをホストできます。
パッケージマネージャーとは、レジストリ(またはその他のソース)からパッケージをダウンロードし、ローカルまたはグローバルにインストールするコマンドラインツールです。パッケージに bin スクリプトが含まれている場合、それらをローカルまたはグローバルに利用可能にします。
最も人気のあるパッケージマネージャーはnpmと呼ばれ、Node.js にバンドルされています。その名前は当初「Node Package Manager」を意味していました。その後、npm と npm レジストリが Node.js パッケージだけでなく使用されるようになると、定義は「npm はパッケージマネージャーではない」に変更されました(ソース)。
yarn や pnpm など、他にも人気のあるパッケージマネージャーがあります。これらのパッケージマネージャーはすべて、デフォルトで npm レジストリを使用します。
npm レジストリの各パッケージには名前があります。2種類の名前があります。
グローバル名はレジストリ全体で一意です。以下に2つの例を示します。
minimatch mocha
スコープ付き名は2つの部分で構成されます:スコープと名前。スコープはグローバルに一意であり、名前はスコープごとに一意です。以下に2つの例を示します。
@babel/core
@rauschma/iterable
スコープは@
記号で始まり、スラッシュで名前と区切られます。
パッケージmy-package
が完全にインストールされると、ほとんどの場合、次のようになります。
my-package/
package.json
node_modules/
[More files]
これらのファイルシステムエントリの目的は何ですか?
package.json
は、すべてのパッケージが持つ必要があるファイルです。node_modules/
は、パッケージの依存関係がインストールされるディレクトリです。各依存関係にも、その依存関係を持つnode_modules
フォルダなどがあります。その結果、依存関係のツリーが作成されます。一部のパッケージには、package.json
の隣にpackage-lock.json
ファイルもあります。これは、インストールされた依存関係の正確なバージョンを記録し、npmを介してさらに依存関係を追加すると更新されます。
package.json
これは、npm を使用して作成できるpackage.json
のスターターです。
{
"name": "my-package",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
これらのプロパティの目的は何ですか?
公開パッケージ(npm レジストリに公開されている)には、いくつかのプロパティが必要です。
name
はこのパッケージの名前を指定します。version
はバージョン管理に使用され、3つのドットで区切られた数値によるセマンティックバージョニングに従います。公開パッケージの他のプロパティはオプションです。
description
、keywords
、author
はオプションであり、パッケージの検索を容易にします。license
はこのパッケージの使用方法を明確にします。パッケージが何らかの方法で公開されている場合は、この値を提供することが理にかなっています。“Choose an open source license”は、この選択を行うのに役立ちます。main
は、ライブラリコードを含むパッケージのプロパティです。これはパッケージ「である」モジュールを指定します(この章の後半で説明します)。
scripts
は、パッケージスクリプト(開発時のシェルコマンドの略記)を設定するためのプロパティです。これらはnpm run
を使用して実行できます。例えば、test
スクリプトはnpm run test
を使用して実行できます。このトピックの詳細については、§15「npmパッケージスクリプトによるクロスプラットフォームタスクの実行」を参照してください。
その他の便利なプロパティ
dependencies
はパッケージの依存関係をリストします。その形式はすぐに説明します。
devDependencies
は、開発のみに必要な依存関係です。
次の設定は、.js
という名前の拡張子を持つすべてのファイルが ECMAScript モジュールとして解釈されることを意味します。レガシーコードを扱っていない限り、追加することが理にかなっています。
"type": "module"
bin
は、bin スクリプトをリストします。npm がシェルスクリプトとしてインストールするパッケージ内の Node.js モジュールです。その形式はすぐに説明します。
license
はパッケージのライセンスを指定します。その形式はすぐに説明します。
通常、name
とversion
のプロパティは必須であり、npmはそれらが欠けている場合に警告します。ただし、次の設定で変更できます。
"private": true
これにより、パッケージが誤って公開されるのを防ぎ、nameとversionを省略できます。
package.json
の詳細については、npm のドキュメントを参照してください。
package.json
の "dependencies" プロパティpackage.json
ファイルの依存関係は次のようになります。
"dependencies": {
"minimatch": "^5.1.0",
"mocha": "^10.0.0"
}
プロパティは、パッケージの名前とバージョンの制約の両方を記録します。
バージョン自体はセマンティックバージョニング標準に従います。最大3つの数値(2番目と3番目の数値はオプションで、デフォルトは0)がドットで区切られます。
Node のバージョン範囲については、semver
リポジトリで説明されています。例としては、
追加の文字がない特定のバージョンは、インストールされたバージョンがバージョンと完全に一致する必要があることを意味します。
"pkg1": "2.0.1",
major.minor.x
またはmajor.x
は、数値であるコンポーネントが一致する必要があり、x
であるか省略されているコンポーネントは任意の値を持つことができることを意味します。
"pkg2": "2.x",
"pkg3": "3.3.x",
*
は任意のバージョンに一致します。
"pkg4": "*",
>=version
は、インストールされたバージョンがversion
以上である必要があることを意味します。
"pkg5": ">=1.0.2",
<=version
は、インストールされたバージョンがversion
以下である必要があることを意味します。
"pkg6": "<=2.3.4",
version1-version2
は>=version1 <=version2
と同じです。
"pkg7": "1.0.0 - 2.9999.9999",
^version
(前の例で使用されている)はキャレット範囲であり、インストールされたバージョンはversion
以上にすることができますが、破壊的な変更を導入してはならないことを意味します。つまり、メジャーバージョンは同じでなければなりません。
"pkg8": "^4.17.21",
package.json
の "bin" プロパティnpm にモジュールをシェルスクリプトとしてインストールするように指示するには、次のようになります。
"bin": {
"my-shell-script": "./src/shell/my-shell-script.mjs",
"another-script": "./src/shell/another-script.mjs"
}
この"bin"
値でパッケージをグローバルにインストールした場合、Node.js はmy-shell-script
とanother-script
コマンドがコマンドラインで使用可能になるようにします。
パッケージをローカルにインストールした場合、パッケージスクリプトまたはnpx
コマンドを使用して、2つのコマンドを使用できます。
文字列も"bin"
の値として許可されます。
{
"name": "my-package",
"bin": "./src/main.mjs"
}
これは次の略記です。
{
"name": "my-package",
"bin": {
"my-package": "./src/main.mjs"
}
}
package.json
の "license" プロパティ"license"
プロパティの値は、常にSPDXライセンスIDを含む文字列です。例えば、次の値は、他の人がどのような条件下でもパッケージを使用する権利を拒否します(パッケージが未公開の場合に便利です)。
"license": "UNLICENSED"
SPDX のウェブサイトには、利用可能なすべてのライセンス ID がリストされています。選択が難しい場合は、「Choose an open source license」のウェブサイトが役立ちます。例えば、「シンプルで寛容なものを求める」場合のアドバイスは次のとおりです。
MITライセンスは短くて簡潔です。これにより、クローズドソースバージョンの作成と配布など、プロジェクトでほとんど何でも行うことができます。
Babel、.NET、Rails は MIT ライセンスを使用しています。
そのライセンスを次のように使用できます。
"license": "MIT"
npm レジストリのパッケージは、通常、2つの異なる方法でアーカイブされます。
いずれの場合も、パッケージは依存関係なしにアーカイブされます。使用するには、事前に依存関係をインストールする必要があります。
パッケージがGitリポジトリに保存されている場合
package-lock.json
が通常は含まれています。パッケージがnpmレジストリに公開されている場合
package-lock.json
はnpmレジストリにアップロードされません。開発依存関係(package.json
のdevDependencies
プロパティ)は、開発中はインストールされますが、npmレジストリからパッケージをインストールする際にはインストールされません。
Gitリポジトリ内の未公開パッケージは、開発中は公開パッケージと同様に扱われることに注意してください。
Gitからパッケージpkg
をインストールするには、そのリポジトリをクローンし、
cd pkg/
npm install
次に、次の手順を実行します。
node_modules
が作成され、依存関係がインストールされます。依存関係のインストールには、その依存関係のダウンロードとその依存関係のインストール(など)も含まれます。package.json
で設定できます。ルートパッケージにpackage-lock.json
ファイルがない場合、インストール時に作成されます(前述のように、依存関係にはこのファイルがありません)。
依存関係ツリーでは、同じ依存関係が複数回存在し、バージョンが異なる可能性があります。重複を最小限に抑える方法はありますが、それはこの章の範囲外です。
これは、依存関係ツリーの問題を解決するための(やや粗雑な)方法です。
cd pkg/
rm -rf node_modules/
rm package-lock.json
npm install
これにより、異なる新しいパッケージがインストールされる可能性があることに注意してください。package-lock.json
を削除しないことで、それを回避できます。
新しいパッケージを設定するためのツールやテクニックはたくさんあります。これは簡単な方法の1つです。
mkdir my-package
cd my-package/
npm init --yes
その後、ディレクトリは次のようになります。
my-package/
package.json
このpackage.json
には、すでに見たスターターコンテンツが含まれています。
現時点では、my-package
には依存関係がありません。ライブラリlodash-es
を使用したいとしましょう。これをパッケージにインストールする方法は次のとおりです。
npm install lodash-es
このコマンドは次の手順を実行します。
パッケージはmy-package/node_modules/lodash-es
にダウンロードされます。
その依存関係もインストールされます。次に、その依存関係の依存関係など。
package.json
に新しいプロパティが追加されます。
"dependencies": {
"lodash-es": "^4.17.21"
}
package-lock.json
は、インストールされた正確なバージョンで更新されます。
他のECMAScriptモジュールのコードは、import
文(A行とB行)を介してアクセスされます。
// Static import
import {namedExport} from 'https://example.com/some-module.js'; // (A)
console.log(namedExport);
// Dynamic import
import('https://example.com/some-module.js') // (B)
.then((moduleNamespace) => {
console.log(moduleNamespace.namedExport);
; })
静的インポートと動的インポートの両方で、*モジュール指定子*を使用してモジュールを参照します。
from
の後の文字列。モジュール指定子には3種類あります。
*絶対指定子*は完全なURLです。たとえば
'https://www.unpkg.com/browse/yargs@17.3.1/browser.mjs'
'file:///opt/nodejs/config.mjs'
絶対指定子は、主にWeb上で直接ホストされているライブラリにアクセスするために使用されます。
*相対指定子*は相対URLです('/'
、'./'
、または'../'
で始まる)。たとえば
'./sibling-module.js'
'../module-in-parent-dir.mjs'
'../../dir/other-module.js'
各モジュールには、その場所によってプロトコルが異なるURLがあります(file:
、https:
など)。相対指定子を使用する場合、JavaScriptはその指定子をモジュールのURLに対して解決することで完全なURLに変換します。
相対指定子は、主に同じコードベース内の他のモジュールにアクセスするために使用されます。
*ベアメ指定子*は、スラッシュでもドットでも始まらないパス(プロトコルとドメインなし)です。パッケージの名前で始まります。これらの名前には、オプションで*サブパス*を付けることができます。
'some-package'
'some-package/sync'
'some-package/util/files/path-tools.js'
ベアメ指定子は、スコープ付きの名前のパッケージも参照できます。
'@some-scope/scoped-name'
'@some-scope/scoped-name/async'
'@some-scope/scoped-name/dir/some-module.mjs'
各ベアメ指定子は、パッケージ内の正確に1つのモジュールを参照します。サブパスがない場合、パッケージの指定された「メイン」モジュールを参照します。ベアメ指定子は直接使用されることはなく、常に*解決*されます。つまり、絶対指定子に変換されます。解決方法はプラットフォームによって異なります。すぐに詳しく説明します。
.js
または.mjs
です。スタイル1:サブパスなし
スタイル2:ファイル名拡張子がないサブパス。この場合、サブパスはパッケージ名の修飾子として機能します。
'my-parser/sync'
'my-parser/async'
'assertions'
'assertions/strict'
スタイル3:ファイル名拡張子を持つサブパス。この場合、パッケージはモジュールの集合と見なされ、サブパスはそのうちの1つを指します。
'large-package/misc/util.js'
'large-package/main/parsing.js'
'large-package/main/printing.js'
スタイル3のベアメ指定子の注意点:ファイル名拡張子の解釈方法は依存関係によって異なり、インポートするパッケージと異なる場合があります。たとえば、インポートするパッケージはESMモジュールに.mjs
、CommonJSモジュールに.js
を使用する場合がありますが、依存関係によってエクスポートされるESMモジュールは、ファイル名拡張子.js
を持つベアメパスを持つ場合があります。
Node.jsでモジュール指定子がどのように機能するかを見てみましょう。
Node.jsの解決アルゴリズムは次のとおりです。
これがアルゴリズムです。
指定子が絶対的な場合、解決はすでに完了しています。3つのプロトコルが最も一般的です。
file:
https:
node:
(後で説明します)指定子が相対的な場合、インポートするモジュールのURLに対して解決されます。
指定子がベアメの場合
'#'
で始まる場合、*パッケージインポート*(後で説明します)の中から検索し、結果を解決することで解決されます。
そうでない場合、次の形式のいずれかのベアメ指定子です(サブパスは省略可能です)。
«package»/sub/path
@«scope»/«scoped-package»/sub/path
解決アルゴリズムは、現在のディレクトリとその祖先をトラバースし、ベアメ指定子の先頭と一致するサブディレクトリ(つまり、次のいずれか)を持つnode_modules
ディレクトリを見つけます。
node_modules/«package»/
node_modules/@«scope»/«scoped-package»/
そのディレクトリはパッケージのディレクトリです。デフォルトでは、パッケージIDの後の(空の可能性のある)サブパスは、パッケージディレクトリに対する相対パスとして解釈されます。デフォルトは、次に説明する*パッケージエクスポート*でオーバーライドできます。
解決アルゴリズムの結果は、ファイルを参照する必要があります。そのため、絶対指定子と相対指定子には常にファイル名拡張子が付いているのです。ベアメ指定子にはほとんどファイル名拡張子が付いていないのは、パッケージエクスポートで検索される略語だからです。
モジュールファイルには通常、これらのファイル名拡張子が付いています。
.mjs
という名前の拡張子が付いている場合、常にESモジュールです。.js
であるファイルは、最も近いpackage.json
に次のエントリがある場合、ESモジュールです。"type": "module"
Node.jsがstdin、--eval
、または--print
を介して提供されたコードを実行する場合、ESモジュールとして解釈されるように、次のコマンドラインオプションを使用します。
--input-type=module
このセクションでは、次のファイルレイアウトを持つパッケージを扱っています。
my-lib/
dist/
src/
main.js
util/
errors.js
internal/
internal-module.js
test/
パッケージエクスポートは、package.json
のプロパティ"exports"
を介して指定され、2つの重要な機能をサポートします。
"exports"
プロパティがない場合、パッケージmy-lib
のすべてのモジュールは、パッケージ名の後の相対パスを介してアクセスできます。例:
'my-lib/dist/src/internal/internal-module.js'
プロパティが存在すると、そこにリストされている指定子のみを使用できます。それ以外は外部から隠されます。
ベアメ指定子の3つのスタイルを思い出してください。
パッケージエクスポートは、これら3つのスタイルすべてに役立ちます。
package.json
:
{
"main": "./dist/src/main.js",
"exports": {
".": "./dist/src/main.js"
}
}
(古いバンドラとNode.js 12以前との)下位互換性のために"main"
のみを提供します。それ以外の場合、"."
のエントリで十分です。
これらのパッケージエクスポートを使用すると、次のようにmy-lib
からインポートできます。
import {someFunction} from 'my-lib';
このファイルからsomeFunction()
をインポートします。
my-lib/dist/src/main.js
package.json
:
{
"exports": {
"./util/errors": "./dist/src/util/errors.js"
}
}
指定子サブパス'util/errors'
をモジュールファイルにマッピングしています。これにより、次のインポートが可能になります。
import {UserError} from 'my-lib/util/errors';
前のセクションでは、拡張子がないサブパスに対する単一のマッピングを作成する方法を説明しました。単一のエントリを介して複数のそのようなマッピングを作成する方法もあります。
package.json
:
{
"exports": {
"./lib/*": "./dist/src/*.js"
}
}
./dist/src/
の子孫であるファイルは、ファイル名拡張子なしでインポートできます。
import {someFunction} from 'my-lib/lib/main';
import {UserError} from 'my-lib/lib/util/errors';
この"exports"
エントリのアスタリスクに注意してください。
"./lib/*": "./dist/src/*.js"
これらは、ファイルパスの断片と一致するワイルドカードではなく、サブパスを実際のパスにマッピングする方法に関する追加の指示です。
package.json
:
{
"exports": {
"./util/errors.js": "./dist/src/util/errors.js"
}
}
指定子サブパス'util/errors.js'
をモジュールファイルにマッピングしています。これにより、次のインポートが可能になります。
import {UserError} from 'my-lib/util/errors.js';
package.json
:
{
"exports": {
"./*": "./dist/src/*"
}
}
ここでは、my-package/dist/src
の下のサブツリー全体のモジュール指定子を短縮しています。
import {InternalError} from 'my-package/util/errors.js';
エクスポートがない場合、インポート文は次のようになります。
import {InternalError} from 'my-package/dist/src/util/errors.js';
この"exports"
エントリのアスタリスクに注意してください。
"./*": "./dist/src/*"
これらはファイルシステムのglobではなく、外部モジュール指定子を内部モジュール指定子にマッピングする方法に関する指示です。
次のトリックにより、my-package/dist/src/internal/
を除いて、my-package/dist/src/
内のすべてを公開します。
"exports": {
"./*": "./dist/src/*",
"./internal/*": null
}
このトリックは、ファイル名拡張子なしでサブツリーをエクスポートする場合にも機能することに注意してください。
エクスポートを条件付きにすることもできます。つまり、特定のパスは、パッケージが使用されるコンテキストに応じて異なる値にマッピングされます。
**Node.jsとブラウザ。**たとえば、Node.jsとブラウザに対して異なる実装を提供できます。
"exports": {
".": {
"node": "./main-node.js",
"browser": "./main-browser.js",
"default": "./main-browser.js"
}
}
"default"
条件は、他のキーと一致しない場合に一致し、最後に配置する必要があります。プラットフォームを区別する場合、新規または未知のプラットフォームにも対応できるため、推奨されます。
開発環境と本番環境 条件付きパッケージエクスポートのもう1つのユースケースは、「開発」環境と「本番」環境を切り替えることです。
"exports": {
".": {
"development": "./main-development.js",
"production": "./main-production.js",
}
}
Node.jsでは、次のように環境を指定できます。
node --conditions development app.mjs
パッケージインポート を使用すると、パッケージは、モジュール指定子に対する省略形を定義できます。これはパッケージ自体が内部で使用し(パッケージエクスポートは他のパッケージの省略形を定義します)、例を示します。
package.json
:
{
"imports": {
"#some-pkg": {
"node": "some-pkg-node-native",
"default": "./polyfills/some-pkg-polyfill.js"
}
},
"dependencies": {
"some-pkg-node-native": "^1.2.3"
}
}
パッケージインポート#
は条件付きです(条件付きパッケージエクスポートと同じ機能を備えています)。
現在のパッケージがNode.jsで使用されている場合、モジュール指定子'#some-pkg'
はパッケージsome-pkg-node-native
を参照します。
それ以外の場合は、'#some-pkg'
は、現在のパッケージ内のファイル./polyfills/some-pkg-polyfill.js
を参照します。
(外部パッケージを参照できるのはパッケージインポートのみです。パッケージエクスポートではできません。)
パッケージインポートのユースケースは何ですか?
バンドラーでパッケージインポートを使用する際は注意してください。この機能は比較的新しいものであり、バンドラーがサポートしていない可能性があります。
node:
プロトコルインポートNode.jsには、'path'
や'fs'
など、多くの組み込みモジュールがあります。これらはすべて、ESモジュールとCommonJSモジュールの両方として利用できます。これらに関する1つの問題は、node_modules
にインストールされたモジュールによって上書きされる可能性があることです。これは、(誤って発生した場合)セキュリティリスクであり、Node.jsが将来新しい組み込みモジュールを導入しようとした場合に、その名前がnpmパッケージによって既に使用されている場合にも問題となります。
組み込みモジュールをインポートしたいことを明確にするために、node:
プロトコルを使用できます。たとえば、次の2つのインポートステートメントはほぼ同等です('fs'
という名前のnpmモジュールがインストールされていない場合)。
import * as fs from 'node:fs/promises';
import * as fs from 'fs/promises';
node:
プロトコルを使用するもう1つの利点は、インポートされたモジュールが組み込みモジュールであることがすぐにわかることです。組み込みモジュールは非常に多いため、コードを読む際に役立ちます。
node:
指定子はプロトコルを持つため、絶対パスと見なされます。そのため、node_modules
では検索されません。