はじめに
npmパッケージを開発するとき、パッケージ利用者の実行環境に合わせて適切なモジュール形式のファイルをパッケージに含め、提供する必要があります。
具体的には、たとえば以下のようなバリエーションが考えられます。
- Node.js環境であれば CommonJS 形式 (
module.exports/require()) - ブラウザ環境で、webpackやRollupなどのモジュールバンドラーを前提とするならば CommonJS や ES Modules 形式 (
export/import) - ブラウザ環境で、モジュールバンドラーなどは使わず
<script>タグでファイルを読み込んで利用するならば UMD 形式
このとき、パッケージ提供側はどういったファイルをパッケージに含めるべきなのか、またそれを TypeScript でどのように実現できるのかがあまりよくわかっていなかったので、整理しました。
注:今回想定していない環境
- AMD (Asynchronous Module Definition) 形式
- Node.js環境でES Modules形式でファイルを扱う
- ブラウザ環境でES Modules形式のファイルをそのまま扱う
<script type="module">
目次
最初にまとめ
最終的なディレクトリ構成は以下のようになりました。

また、package.jsonには以下main, module, types, filesフィールドを指定します。
// package.json { "main": "lib/index.js", "module": "esm/index.js", "types": "lib/index.d.js", "files": [ "lib", "esm", "umd" ], // ... }
1. CommonJS 形式
CommonJSはNode.js環境で採用されているモジュール形式であることと、ブラウザ環境でもwebpackなどのモジュールバンドラーやTypeScriptがこの形式をサポートしていることから、
とりあえずCommonJS形式でパッケージを提供しておけばNode.js環境とブラウザ環境を両方サポートできます。
tsconfig.json
TypeScriptでCommonJS形式でコンパイルする場合、tsconfig.jsonのmoduleプロパティにcommonjsを指定します。
また、出力先のディレクトリ名は任意ですが、ここではlibとしてoutDirプロパティに指定しておきます。
// tsconfig.json { "compilerOptions": { "module": "commonjs", "outDir": "./lib" // ... } }
こうすると、ソースファイルを格納しているディレクトリ(ここではsrc)と同じ構成のまま、
TypeScript → JavaScript にコンパイルした結果がlibディレクトリに出力されます。
- src/ - index.ts - moduleA.ts - moduleB.ts - lib/ - index.js - moduleA.js - moduleB.js
package.json
package.jsonでは、mainフィールドにパッケージのエントリーポイントとなるファイルのパスを、filesに出力先ディレクトリlibを記載します。
// package.json { "main": "lib/index.js", "files": [ "lib" ] }
2. ES Modules 形式
webpack(v2以降)やRollupなどのモジュールバンドラーには、Tree Shakingという機能が備わっています。
これは、exportしているがどこからもimportされていない不要なモジュールをバンドル時に削除する機能です。
参考:webpackの公式ドキュメント内でTree Shakingについて説明されているページ
パッケージ提供側が多数のモジュールを提供している場合、ES Modules形式つまり import/export で書かれたファイルをCommonJS形式に変換せずそのまま提供することで、利用者側はTree Shakingの恩恵を受けることができます。
tsconfig.json
TypeScriptでこれを実現する場合、CommonJSと同様にmoduleプロパティを使用します。
先ほどはcommonjsという値を指定しましたが、今度はESNextを指定します。
また、先ほどのCommonJS形式のファイルと出力先のディレクトリを分けるため、outDir: "./esm"も指定します。同じくディレクトリ名は任意です。
// tsconfig.json { "compilerOptions": { "module": "ESNext", "outDir": "./esm" // ... } }
こうすることで、CommonJS形式と同じくsrc下と同じ構成のファイル群のJSがesmに生成されます。
- src/ - index.ts - moduleA.ts - moduleB.ts - lib/ # 中身はCommonJS( module.exports / require ) - index.js - moduleA.js - moduleB.js - esm/ # 中身はES Modules( export / import ) - index.js - moduleA.js - moduleB.js
NOTE: tsconfig.jsonの構成をどのようにすべきか
このように、同じTypeScriptのソースファイルから複数の設定でコンパイルする場合、プロパティの異なる複数のtsconfig.jsonが必要になります。
tsconfig.jsonはextendsというプロパティがあり、これを使うとある設定ファイルの内容を継承して一部だけ上書きできます。
モジュール形式によらないプロパティはベースの設定ファイルに記述し、モジュール形式によって差が出るプロパティはベースを継承した別の設定ファイルを定義するのが良いかと思います。
// tsconfig.base.json { "compilerOptions": { "target": "es5", "strict": true, // ... } }
// tsconfig.esm.json { "extends": "./tsconfig.base", "compilerOptions": { "module": "ESNext", "outDir": "./esm" } }
また、コンパイル時は-pまたは--projectオプションで使用する設定ファイルを指定できるので、これをpackage.jsonのscriptsに記載しておくと良いでしょう。
// package.json { "scripts": { "build": "npm run build:cjs && npm run build:esm", // CommonJS用 "build:cjs": "tsc -p tsconfig.cjs.json", // ES Modules 用 "build:esm": "tsc -p tsconfig.esm.json" } }
詳しくはサンプルリポジトリを参考にしてください。
package.json
package.jsonには、CommonJS形式のときと同じくfilesフィールドに出力したディレクトリ(esm)を追加するほか、moduleフィールドでesm内のエントリーポイントを指定します。
// package.json { "main": "lib/index.js", "module": "esm/index.js", "files": [ "lib", "esm" ] }
moduleフィールドについて、公式ドキュメントには載っていませんでしたが、以下のStack Overflowが見つかりました。
Node.jsでES Modulesを扱うためのProposalということでしょうか...(あんまりわかってない)
また、Rollupのドキュメントには以下の記述があります。
https://rollupjs.org/guide/en/#publishing-es-modules
If your
package.jsonfile also has amodulefield, ESM-aware tools like Rollup and webpack 2+ will import the ES module version directly.
つまり、モジュールバンドラーはこのフィールドで指定したパスのファイルをES Modules形式で扱うようです。
3. UMD
Reactなどのメジャーなライブラリでは、インストール方法として npm install だけではなく以下のようにCDNで利用できる形式をサポートしています。
https://reactjs.org/docs/cdn-links.html
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script> <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
これをHTMLに書くとReactやReactDOMがグローバル変数に定義され、使えるようになるため、利用者側はモジュールバンドラーを使った環境を構築する必要がありません。
オンラインエディターなどでも簡単に試すことができます。
これを実現するためのモジュール形式がUMDです。
webpack.config.js
パッケージをUMD形式で配布するためには最終的にファイルを1つにバンドルする必要があり、これはTypeScript単体では完結しません。何らかのモジュールバンドラーを利用する必要があります。
ここではwebpackを例にします。
webpackでUMD形式のファイルを作成するには、output.libraryおよびoutput.libraryTargetフィールドを使用します。
https://webpack.js.org/configuration/output/#outputlibrary
https://webpack.js.org/configuration/output/#outputlibrarytarget
// webpack.config.js const path = require("path"); module.exports = { mode: "production", entry: "./src/index.ts", output: { path: path.resolve(__dirname, "umd"), filename: "my-typescript-package.js", library: "MyTsPackage", libraryTarget: "umd" }, module: { rules: [ { test: /\.ts(x*)?$/, exclude: /node_modules/, use: { loader: "ts-loader", options: { configFile: "tsconfig.umd.json" } } } ] }, resolve: { extensions: [".ts", ".tsx", ".js"] } };
libraryTargetにumdを指定することでUMD形式でのバンドルを行います。またバンドルしたファイルを読み込んだときにlibraryに指定したグローバル変数名でアクセスできるようになります。
なお、webpackを使用する場合、TypeScriptもwebpack内で使用するためts-loaderが必要となります。
options.configFileは設定ファイルがtsconfig.jsonという名前の場合は不要です。
tsconfig.json
tsconfig.json側は特別な設定は必要ありません。出力先のディレクトリも今回はwebpack.config.js側で指定しているため不要です。
ただ、
最新版TypeScript+webpack 4の環境構築まとめ(React, Vue.js, Three.jsのサンプル付き) - ICS MEDIA
にもあるように、moduleにESNextを指定していないと、ソースファイル内に使用していないexportが混ざっていてもTree Shakingが機能せず、バンドルファイルに含まれることになります。
(これはアプリケーションを開発する際は重要ですが、npmパッケージとして提供したいようなライブラリの場合は使っていないexportは基本ないはず...)
また、ベースの設定ファイルでdeclaration: trueにして型定義ファイルを出力するようにしている場合、UMD用の設定では出力をOFF(declaration: false)にしておくのが良いと思います。
package.json
filesにumdディレクトリを追加することと、UMDビルド用のスクリプトを追加すること以外は特にありません。
"scripts": {
- "build": "npm run build:cjs && npm run build:esm",
+ "build": "npm run build:cjs && npm run build:esm && npm run build:umd",
"build:cjs": "tsc -p tsconfig.cjs.json",
"build:esm": "tsc -p tsconfig.esm.json",
+ "build:umd": "webpack",
"test": "echo \"Error: no test specified\" && exit 1"
},
"files": [
"lib",
- "esm"
+ "esm",
+ "umd"
],
ここまでで、CommonJS、ES Modules、UMDのファイルが以下のような構成で出力されるようになっています。
- src/ - index.ts - moduleA.ts - moduleB.ts - lib/ # 中身はCommonJS( module.exports / require ) - index.js - moduleA.js - moduleB.js - esm/ # 中身はES Modules( export / import ) - index.js - moduleA.js - moduleB.js - umd/ # UMD - my-typescript-package.js
NOTE: development用とproduction用の2つのバンドルを用意する場合は?
先ほど例として挙げたReactもそうですが、UMD形式に対応しているライブラリはdevelopment用 (foo.development.js)とproduction用(foo.production.js)の2種類を提供していることがあります。
https://reactjs.org/docs/cdn-links.html

(試していませんが) これを実現したい場合、webpackの
Configuration Types > Exporting multiple configurations
によると、webpack.config.jsで配列を返すようにすれば複数の設定でバンドルした結果を出力してくれます。
modeやoutput.filenameだけ異なる2つの設定を用意し、まとめてumd/以下に出力すれば良いでしょう。
NOTE: 出力先のディレクトリ名について
UMD形式についてもディレクトリ名は任意です。
が、umdまたはdistという名前にしておくと、unpkg.comというCDNがnpm publish時に自動的に認識してくれ、publishしたパッケージが
https://unpkg.com/<package name>@<version>/<`umd` or `dist`>/<file name>
というURLで利用できるようになるので、この慣習にならっておくのがおすすめです。
参考:https://unpkg.com/#workflow

4. (おまけ) 型定義ファイル(.d.ts)
モジュール形式とは別の話ですが、TypeScriptで開発したパッケージの場合、本体のスクリプトと一緒に型定義ファイルもパッケージに含めて提供してあげる必要があります。
必要なことは
の2点です。
後者は、たとえばlib/ディレクトリ以下に
- src/ - index.ts - moduleA.ts - moduleB.ts - lib/ - index.js - index.d.ts - moduleA.js - moduleA.d.ts - moduleB.js - moduleB.d.ts
というように型定義ファイルを出力するようにしている場合、
// package.json { "main": "lib/index.js", "types": "lib/index.d.js" }
となります。
※サンプルリポジトリではlib/にもesm/にも型定義ファイルを出力するようになっていますが、どちらかだけで十分のはず
余談
上に書きませんでしたが、調べている過程で見つけたTips的な情報を。
package.jsonのfilesフィールドは必ず必要なのか
filesはnpm publish時にパッケージに含めるファイルやディレクトリを指定するためのフィールドで、指定しなかった場合は全てのファイルがパッケージに含められます。
そのため、余計なファイルが含まれることを許容すればこのフィールドは指定しなくても良さそうに思えます。
が、 https://docs.npmjs.com/files/package.json#files を読むと
You can also provide a
.npmignorefile in the root of your package or in subdirectories, which will keep files from being included. (中略) If there is a.gitignorefile, and.npmignoreis missing,.gitignore’s contents will be used instead.
とあります。つまり
.npmignoreに列挙したファイルやディレクトリはパッケージから除外される.npmignoreを置いていない場合、.gitignoreの内容が使われる
ということです。
今回のlibのようにコンパイルしたファイルの出力先はgit commitしないよう.gitignoreに記載するのが一般的だと思われるので、filesの指定がないと意図せずパッケージにも含まれなくなってしまいます。
このような事故を避けるため、またパッケージの内容を明示的にするためにもfilesは記載しておくのがおすすめです。
package.jsonのunpkgフィールドとは
UMD形式をサポートしているパッケージをいくつか調べたところ、package.jsonにunpkgというフィールドを指定しているパッケージがありました。
これについては unpkg.com のサイトを確認したところ
If you omit the file path (i.e. use a “bare” URL), unpkg will serve the file specified by the
unpkgfield inpackage.json, or fall back tomain.
とあるので、UMD形式で出力したファイルのパスを指定しておけば、利用者は
https://unpkg.com/<package name>@<version>
というURLでパッケージを読み込めるというわけですね。
(あんましなさそうですが)
webpack.config.jsのexternalsオプションとは
これもUMD形式をサポートしているパッケージでよく見かけました。
参考:
- Semantic-UI-React
- design-system-react
// webpack.config.js externals: { react: 'React', 'react-dom': 'ReactDOM', } // または externals: { react: { amd: 'react', commonjs: 'react', commonjs2: 'react', root: 'React', }, // ... }
これについてはwebpackの公式ドキュメント
https://webpack.js.org/configuration/externals/
によると、このオプションで指定したモジュールはバンドルしたファイルに含まれなくなり、パッケージ利用者側が別途モジュールを読み込む必要があります。
リファレンス
モジュール形式については以下が参考になりました。
- イマドキのnpmでは何を配布すべきか - Qiita
- "module"フィールド対応 · Webフロントエンド パフォーマンス改善ハンドブック
- npm で公開するブラウザー向けパッケージのファイル構成は Redux を参考にしよう | パークのソフトウエア開発者ブログ
- JavaScript モジュールの現状 | POSTD
参考にしたライブラリ
- React
- ビルドはRollupでやってるのでこのあたり https://github.com/facebook/react/tree/master/scripts/rollup
- Material-UI
- design-system-react
- SmartHR-UI
- React Bootstrap
- Semantic UI React
- Babel + webpack
- CommonJS, ESM, UMDをサポート
- ビルドはgulp: https://github.com/Semantic-Org/Semantic-UI-React/blob/master/gulp/tasks/dist.js