30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

npm パッケージ はどのように利用されるのか

npm パッケージは、Node.js アプリによって使用されるだけでなく、モジュールバンドラがバンドルを行う際に使用されたり、CDN 経由でブラウザから使用されたりする。
この記事ではそれぞれのケースにおいて、npm パッケージとして配布されるファイルのうち、どのファイルがどのように利用されるのか見ていく。
これを理解していないと、npm パッケージを公開する際に何をどのように配布するべきなのかも分からない。

動作確認に使った各環境のバージョンは、以下の通り。

  • Node.js14.7.0
  • npm6.14.7
  • Google Chrome84.0.4147.125

また、モジュールバンドラの例として webpack を使った検証を行っているが、そのバージョンは以下の通り。

  • webpack4.44.1
  • webpack-cli3.3.12

型定義ファイルについては以下の記事で説明したので、本記事では割愛する。

numb86-tech.hatenablog.com

検証環境の準備

Yarn のワークスペースを使って、環境を作る。

ワークスペースについても、以前書いた。

numb86-tech.hatenablog.com

今回検証したいのは「npm パッケージがどのように利用されるのか」なので、パッケージと、それを利用するアプリを用意する。
パッケージの名前はmy-package、アプリの名前はmy-appとする。

まず、ルートディレクトリに以下の内容のpackage.jsonを作る。

{
  "private": true,
  "workspaces": ["my-package"]
}

これで、my-packageがワークスペースの対象になった。
早速、以下の内容のmy-package/package.jsonmy-package/main.jsを作る。

{
  "name": "my-package",
  "version": "1.0.0",
  "main": "./main.js"
}
module.exports = {
  x: 1,
}

続いて、以下の内容のmy-app/package.jsonを作る。

{
  "name": "my-app",
  "dependencies": {
    "my-package": "1.0.0"
  }
}

そしてルートディレクトリで$ yarnを実行すると、以下のような構成になる。

├── my-app
│   ├── index.js
│   └── package.json
├── my-package
│   ├── main.js
│   └── package.json
├── node_modules
│   └── my-package -> ../my-package
├── package.json
└── yarn.lock

これで、my-appからmy-packageを利用できるようになった。

Node.js から使用する

以下のmy-app/index.jsを用意する。

const Package = require('my-package');

console.log(Package);

そして$ node my-app/index.jsを実行すると、{ x: 1 }が表示される。

npm パッケージをrequireすると、そのパッケージのpackage.jsonmainフィールドで指定されているファイルを、読み込む。
今回の例ではmy-package/package.jsonmainフィールドに./main.jsを指定したので、my-package/main.jsが読み込まれた。

また、requireではなくimportで読み込むこともできる。

まず、my-app/package.json"type": "module"を追加して、アプリ側のモジュールシステムを ES Modules(以下、ESM)にする。
その上でmy-app/index.jsを以下の内容にして、実行する。

import Package from 'my-package';

console.log(Package);

この場合も、{ x: 1 }が表示される。

この状態で今度は、my-package/package.jsonにも"type": "module"を追加し、my-package/main.jsを以下のように書き換える。

export default 9;

そして$ node my-app/index.jsを実行すると、9が表示される。
これは、ESM で書かれた書かれた npm パッケージを、同じく ESM で書かれたアプリがインポートした形になる。

Node.js におけるモジュールシステムやpackage.jsontypeフィールドについては、以下の記事に書いた。

numb86-tech.hatenablog.com

この記事では、ESM と CommonJS(以下、CJS) の互換性についても書いたが、npm パッケージにおいてもこのルールは変わらない。
例えば、CJS で書かれたモジュールを ESM でimportする場合、名前付きインポートはできない。

つまり、package.jsonmainフィールドで指定されているファイルが読み込まれる、ということ以外は、npm パッケージもそれ以外のファイルも同じように扱われる。

モジュールバンドラから使用する

フロントエンドで使用するコードの場合、モジュールバンドラで複数のファイルを結合することが多い。
その場合、npm パッケージを含む依存関係の解決も、バンドル時に行われる。

このときの挙動は、Node.js アプリから npm パッケージを使用するときのそれとは、異なる。
例えば、必ずしもmainフィールドで指定されているファイルが読み込まれるわけではない。

モジュールバンドラとして webpack を使い、確認していく。

まず、ルートディレクトリで以下を実行して、必要なライブラリをインストールする。

$ yarn add -D -W webpack webpack-cli

そして、ルートディレクトリにwebpack.config.jsを用意する。

const path = require('path');

module.exports = () => {
  return {
    entry: {
      index: './my-app/index.js',
    },
    output: {
      path: path.resolve(__dirname, 'dist'),
    },
    resolve: {
      mainFields: ['module', 'main']
    },
  }
};

この状態で$ yarn run webpack --mode=productionを実行すると、my-app/index.jsをエントリポイントとしてビルドが行われ、dist/index.jsが生成される。

my-app/index.jsは先程と同様に以下の内容にしておく。

import Package from 'my-package';

console.log(Package);

my-package側に、以下のファイルを用意する。

// my-package/main.js
module.exports = {
  x: 1,
}
// my-package/module.js
module.exports = {
  x: 'a',
}

そして、my-package/package.jsonを、以下の内容にする。

{
  "name": "my-package",
  "version": "1.0.0",
  "main": "./main.js",
  "module": "./module.js"
}

この状態でビルドしたdist/index.jsを実行してみる。

$ node dist/index.js
{ x: 'a' }

{ x: 'a' }が出力されているので、moduleフィールドで指定したファイルが読み込まれていることになる。

次に、my-package/package.jsonからmoduleフィールドを削除したうえで、ビルドして実行してみる。

$ node dist/index.js
{ x: 1 }

そうすると、mainフィールドで指定したファイルが読み込まれている。

これは、webpack.config.jsresolve.mainFieldsで指定した順番に、package.jsonのフィールドを探していくため。
['module', 'main']とすると、まずmoduleフィールドを探し、それが見つからなければmainフィールドを探す。
列挙されたフィールド名がひとつもpackage.jsonになかった場合、ビルドに失敗する。

resolve.mainFieldsにはデフォルト値が設定されており、webpack.config.jstargetの値によって決まる。
targetwebworkerweb、もしくは未指定の場合は、resolve.mainFieldsのデフォルト値は['browser', 'module', 'main']になる。
targetがそれ以外の場合は['module', 'main']になる。

ちなみに、webpack 経由で npm パッケージを利用する場合、互換性に関する挙動も変化する。

例えば、ESM から CJS を読み込む場合、Node.js では名前付きインポートができないのだが、webpack で依存関係を解決する場合は可能になる。

// my-package
// module.exports = {
//   x: 1,
// }

// 名前付きインポートがエラーにならない
import {x} from 'my-package';

console.log(x); // 1

CDN からの利用

npm パッケージは、CDN サービスを経由してブラウザで使うこともできる。

例えば、UNPKGという CDN サービスの場合、公式サイトに以下のように書かれており、全ての npm パッケージが配信の対象となっている。

unpkg is a fast, global content delivery network for everything on npm.

具体的には、https://unpkg.com/:package@:version/:fileという URL で、npm パッケージの中身が配布されている。
また、https://unpkg.com/:package@:version/にアクセスすると、配信されているファイルの一覧を見ることができる。

例えば、拙作のken-allの場合、https://unpkg.com/browse/ken-all@0.2.1/ で、v0.2.1におけるファイルの一覧を見ることができる。

f:id:numb_86:20200816234553p:plain

このうち、umd/index.jsはブラウザで使用されることを想定してビルドしたファイルなので、以下のようにブラウザから読み込んで使用することができる。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Ken All Sample</title>
</head>
<body>
  <input id="post-code" maxlength="7">
  <p id="result"></p>

  <script src="https://unpkg.com/ken-all@0.2.1/umd/index.js"></script>
  <script>
    const searchBoxElem = document.querySelector('#post-code');

    searchBoxElem.addEventListener('input', e => {
      const postCode = e.currentTarget.value;

      if (postCode.length === 7) {
        const resultTextElem = document.querySelector('#result');
        KenAll.default(postCode).then(res => {
          if (res.length === 0) {
            resultTextElem.textContent = '該当する住所はありません';
          } else {
            resultTextElem.textContent = res[0].join(' ');
          }
        });
      }
    }, false);
  </script>
</body>
</html>

npm パッケージの中身がそのまま配布されているだけなので、ブラウザで使われることを想定したファイルでない場合、上手く動かない可能性がある。
例えば、https://unpkg.com/ken-all@0.2.1/dist/index.jsをブラウザで読み込むと、ReferenceError: exports is not definedというエラーが出る。

バージョンの指定については、セマンティックバージョニングの範囲指定を行うこともできる。
例えば記事執筆時点でのken-allの場合、^0.2.0を指定すると、0.2.1にリダイレクトされる。

$ curl -I https://unpkg.com/ken-all@^0.2.0/umd/index.js
HTTP/2 302
(省略)
location: /ken-all@0.2.1/umd/index.js

参考資料