ファイル構成は Redux 3.7.2 を、
ファイル名やディレクトリ名は主に React 16.0.0 を参考にしています。
Redux: https://www.jsdelivr.com/package/npm/redux
React: https://www.jsdelivr.com/package/npm/react
結論から言えばこんなファイル構成と package.json になります。
my-package/ src/ -- ソースコード index.ts -- メインモジュール (エントリポイント) submodule1.ts -- 他のモジュールからインポートするモジュール submodule2.ts -- 〃 ... umd/ (or dist/) -- UMD ビルド my-package.js my-package.min.js types/ -- TypeScript 型定義 index.d.ts submodule1.d.ts submodule2.d.ts ... es/ -- ES 2015 モジュール群 index.js submodule1.js submodule2.js ... cjs/ (or lib/) -- CommonJS モジュール群 index.js submodule1.js submodule2.js ... LICENSE README.md package.jsonpackage.json
{ "name": "my-package", "version": "0.0.0", "main": "./cjs/index.js", "module": "./es/index.js", "jsnext:main": "./es/index.js", "types": "./types/index.d.ts", "jsdelivr": "./umd/my-package.min.js", "unpkg": "./umd/my-package.min.js", "files": [ "umd", "cjs", "es", "types", "src" ], ... }
以下、このようにした私の言い分です。
■ ソースコードを提供しよう (src/)
ソースコードを npm パッケージに含めるかどうかは完全に任意です。
Redux はソースコードを含めていますが、 React は含めていません。
個人的には何かトラブったときに読みたくなる可能性があるので含めてほしいです。
■ ️UMD ビルドを提供しよう (umd/ または dist/)
UMD は IIFE + AMD + CommonJS です。
HTML の script タグで読み込む (IIFE) ことも、
スクリプト実行時に非同期で取り込む (AMD) ことも、
モジュールバンドラーでくっつけることもできます。
バンドルしていない CommonJS モジュール群を別途提供する場合 (後述)、
UMD は提供せず AMD は捨てて IIFE を提供するという選択肢もあるかと思います。
が、わざわざ AMD を捨てるより思考停止して UMD を提供すれば良いと私は思います。
(IIFE に対する UMD のオーバーヘッドなんて微々たるものでしょう。)
CommonJS モジュール群を別途提供しない場合は
package.json の main フィールドに UMD ビルドのパスを書きましょう。
https://docs.npmjs.com/files/package.json#main
CDN からの利用を想定するなら
package.json の jsdelivr フィールドと unpkg フィールドに minified UMD ビルドのパスを書きましょう。
https://www.jsdelivr.com/features#publishing-packages
https://unpkg.com
ディレクトリ名は Redux では dist/ ですが React では umd/ です。
(React 15 では dist/ でしたが 16 で umd/ に変更されています。)
私は dist/ という言葉に意味を感じられないので (他のファイルも distribution には違いないので) umd/ としています。
■ TypeScript 型定義を提供しよう (index.d.ts または types/)
お願いします (私が一応 TSer なので)。
そして
package.json の types フィールド (または typings フィールド) にメインモジュールの型定義のパスを書きましょう。
https://www.typescriptlang.org/docs/handbook/declaration-files/publishing.html
ディレクトリ名として typings という言葉も使えそうですが、
TypeScript の黒歴史を彷彿とさせるので私は避けています。
https://github.com/typings/typings
Redux ではパッケージルートに index.d.ts が置かれています。
ちな Flow の型定義は見当たりませんでした。
React は Facebook なので Flow 推しだと思うのですが、
TypeScript の型定義も Flow の型定義も見当たりませんでした。
■ ️ES 2015 モジュールを提供しよう (es/)
ES 2015 モジュールの時代がやってきました (やっと...きました)。
Rollup はプラグインなしでは ES 2015 形式のモジュールじゃないと読み込めません。
⇒ https://github.com/rollup/rollup-plugin-commonjs
webpack 2+ は ES 2015 形式のモジュールじゃないと Tree Shaking できないようです。
⇒ https://github.com/indutny/webpack-common-shake
主要ブラウザーは import ステートメントを実装し始めているようです。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/import
# NodeJS でも 8.5.0 で ES モジュール機能が実装されましたが、 9.1.0 現在も実行時オプションに --experimental-modules フラグを付けた上で拡張子を .mjs にする必要があります (https://nodejs.org/api/esm.html)。
# 今回はブラウザー向けパッケージの話に絞ります。
ということで ES 2015 モジュールを提供しましょう。
そして
package.json の module フィールドと jsnext:main フィールドにメインモジュールのパスを書きましょう (jsnext:main はもう要らないかもですが一応)。
https://github.com/rollup/rollup-plugin-node-resolve
https://webpack.js.org/configuration/resolve/#resolve-mainfields
重要な注意点として、モジュール形式は ES 2015 でも、使用できる言語機能やライブラリはサポート対象とする実行環境次第 (IE 11 を含めるなら ES 5 相当) になるかと思います。
Rollup や webpack 2+ は ES 2015 の import 構文を解析してモジュールをくっつけることはできますが、他の構文のトランスパイルや Polyfill の提供はしないからです。
たとえば Object#assign() を使いたい場合、次のいずれかの対処が必要になるかと思います。
* Ponyfill を dependencies に含めてインポートする
* Polyfill を読み込んでから自身のモジュールを利用するよう利用者に懇願する
* IE 11 はサポート対象外だと明言する
結局モジュール形式以外は CommonJS モジュール群を提供していたこれまでと同じです。
# 私は依存パッケージの数だけ Object#assign() の代替実装が読み込まれているような気がしてなりません。
# 最終的に利用するアプリでは core-js や babel-polyfill を読み込むかもしれないのに...
■ ️CommonJS モジュール群は必要に応じて提供しよう (cjs/ または lib/)
ES 2015 モジュールが読み込めない古いモジュールバンドラー (browserify とか webpack 1.x とか) や NodeJS 向けに、バンドルしていない CommonJS 形式のモジュールを提供する場合があります。
UMD ビルドも CommonJS モジュールとして読み込めますが、未バンドルの CommonJS モジュール群も別途提供した方が良い場合が多いと思います。理由は以下。
たとえばパッケージ a, b のメインモジュールがいずれもパッケージ z を読み込んでいるとします。
var a = require('a');
var b = require('b');
と書いたとき、
もし a, b がいずれも未バンドルの CommonJS であれば、 require('z') は 2 回呼ばれますが、実質的に z が読み込まれるのは 1 回です。
もし a がバンドル済み (UMD ビルド) であれば、すでに z は a に「組み込まれて」しまっており、 b のメインモジュールはそれを知らずに require('z') するので、 z は 2 回読み込まれることになります。
依存パッケージが全くないのであれば UMD で十分かと思いますが、
そうでないなら UMD ビルドではとても効率が悪くなってしまう可能性があるので、
未バンドルの CommonJS モジュール群も提供しましょう。
そして
package.json の main フィールドにメインモジュールのパスを書きましょう。
https://docs.npmjs.com/files/package.json#main
https://webpack.github.io/docs/configuration.html#resolve-packagemains
https://github.com/browserify/browserify#packagejson
ディレクトリ名は Redux では lib/ ですが React では cjs/ です。
(React 15 では lib/ でしたが 16 で cjs/ に変更されています。)
私は lib/ という言葉に意味を感じられないので cjs/ としています。
ディレクトリを掘らずパッケージルートに CommonJS モジュール群を置いているパッケージもあります。
たとえば react-router (4.2.0 現在)
https://www.jsdelivr.com/package/npm/react-router
次のように、複数のモジュールを必要に応じて個別に読み込むようなパッケージ (メインモジュールが「唯一のエントリポイント」ではないパッケージ) では便利かもしれません。
var module1 = require('that-package/module1');
var module3 = require('that-package/module3');
■ 結論
結論は最初に書きました。
ちな実際に個人的に npm でパッケージを公開してみましたが、
他のパッケージに依存しなかったので CommonJS モジュール群は含めませんでした。
https://www.npmjs.com/package/ripplet.js
https://www.jsdelivr.com/package/npm/ripplet.js
以上、ちかでした
