https://github.com/ArakiTakaki/qiita-example-npm-microrepo
サンプルレポジトリ
概要
案件特化のライブラリを制作したくなるケース、存在しませんか?
単一ファイルの場合は、1ファイルで終わりですが、必ずしも単一ファイルで終わりなわけではありません。
対応するにも懸念点が多かったりもします。
- browserへの対応
hogehoge.min.js
- commonjsへの対応
hogehoeg.cjs.js
- ESModuleへの対応
hogehoge.es.js
Webpackの場合、これらのだし分けが苦労したため、今回は、Rollup
というバンドラーを用い、ライブラリの基礎を作成していくことにしました。
今回使用するRollup
はReact
やbabel
で使用されており、設定が非常に軽微であるためこれを使用しました。
他にも下記のライブラリがRollup
で組まれている
- Vue
- Ember
- Preact
- D3
- Three.js
- Moment
なぜ出し分けが必要なのか
- NodeJSでサラッとアプリを作る際に
import
とexport
構文が邪魔になる。 - TypeScriptを使用している際に、
commonjs
でないとimport
できない- CommonJSだと
tree shaking
が使用できない
- CommonJSだと
- CDNサーバーを公開して、browserに直接ライブラリ提供を行いたい
上記を柔軟に対応するため、出し分けを行えるように作成したほうが利用者に対し、やさしいためです。
当初lodash
は、CommonJS
のみに対応していた(と思う)ため、tree shaking
などが行えない、などの弊害がありましたが
のちにlodash-es
が開発されたという事もあり、両方の対応(特にESModule)の対応は結構大事だと思います。
使用するライブラリ
- typescript
- babel
- rollup
マイクロレポジトリとは
1レポジトリに対して、1ライブラリを管理する手法です。
良い部分
- 同一レポジトリの依存関係が無く(そりゃそう)見通しが良い
- 基本的にすべてのnpmコマンドは1レポジトリを起点に作成されているため、コマンドをスクリプトで記述する必要がない
悪い部分
- パッケージを分けたい場合、柔軟ではない
- 例)
react-router
とreact-router-dom
のような分け方をする際適していない - 例) https://github.com/OnsenUI/OnsenUI のように forReact forVue などコアライブラリに対して派生させるのには向いていない
- 上記に関しては別レポジトリに分けても良いとは思ってるため議論するべき項目
- 例)
今回なぜrollup
を使用するのか
場合分けアウトプットが非常に楽になるためです。
利用者は下記の3種類考えられると思っています
- Broserからscriptタグを直接使用したい
-
import / export
構文のモジュールを使用してTree Shaking
を使用したい -
commonjs
を使用し、require('hogehoge')
という風に記述したい
webapckでのだし分け
rollupでのだし分け
rollupの場合、非常に簡素に、出し分けが行えます。
バージョニングに関して
NPMパッケージを利用する際にパッケージのバージョンは結構気にすると思いますが、こちらにも規則があります。
NPMはsemverという手法を用いてバージョン管理が行われております。
概要
"@babel/plugin-proposal-class-properties": "7.8.3",
こちらを参考に分解するのであれば
Majorは7 Minorは8 Patchは3 となります。
Major Version
後方互換を伴わない破壊的なアップデートが行われる際などはこちらを使用します。
- 機能の削除
- 引数の変更
注意として、割と有名なライブラリでも、このsemverの規則を守らないものがあったりなんやり。
Backbone.js - Follow SemVer #2888
So, as I like to joke — not "semantic" versioning, romantic versioning.
バージョンアップコマンド: npm version major
Minor Version
後方互換があり機能の追加が行われる際などはこちらを使用します。
- 機能の追加
純粋に、機能の追加であればデグレは発生しないが、Major Version
が上がった事により、注目や関心を持たれるようにするという印象です。
バージョンアップコマンド: npm version minor
Patch Version
後方互換があり、バグの修正・パフォーマンスチューニングが行われる際などはこちらを使用します。
純粋に処理が重たい部分を細かくする。
引数、返り値が変更されず、バグをつぶした状態です。
バージョンアップコマンド: npm version patch
実際の制作
rollupのコンフィグ
- rollup
- @rollup/plugin-commonjs
- @rollup/plugin-node-resolve
- @rollup/plugin-babel
example(cjs mjs)
import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import babel from '@rollup/plugin-babel';
const extensions = [ '.js', '.jsx', '.ts', '.tsx' ];
const packageName = 'hogePackage';
export default {
input: './src/index.ts',
output: [
{
// CommonJS
file: `dist/${packageName}.js`,
format: 'cjs',
},
{
// ES Module
file: `dist/${packageName}.esm.js`,
format: 'es',
},
{
// min.js
file: `dist/${packageName}.min.js`,
format: 'iife',
name: packageName,
},
],
plugins: [
resolve({
extensions,
}),
commonjs(),
babel({
extensions,
include: ['src/**/*'],
})
],
};
{
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"noFallthroughCasesInSwitch": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"esModuleInterop": true,
"noUnusedLocals": true,
"noImplicitAny": true,
"declarationDir": "dist/@types",
"declaration": true,
"target": "esnext",
"module": "esnext",
"strict": true
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist"
]
}
// .babelrc
{
"presets": [
"@babel/preset-env",
"@babel/preset-typescript"
],
"plugins": [
"@babel/proposal-class-properties",
"@babel/proposal-object-rest-spread"
]
}
NPMへの公開タスク
ターミナルから会員登録が可能です
$ npm set init.author.name "araki takaki"
$ npm set init.author.email "arakitakaki@team-lab.com"
$ npm set init.author.url "http://qiita.com/ArakiTakaki"
$ npm adduser # 会員登録情報の入力
## まだユーザが登録されていない場合は、npm に登録する新規ユーザ情報を入力する。
## 既にnpm ユーザが作成されている場合はそのユーザの情報を入れて、ログインする。
Organizationを追加しよう
package.jsonの編集
{
"name": "@自分の作成したNPMオーガナイゼーション/自分の作成したパッケージ名",
"version": "0.0.1", // 所定のバージョン
"description": "パッケージの名前",
"keywords": [],
"private": false, // パッケージを公開するためfalse
"license": "MIT",
"scripts": {
"prebuild": "rimraf dist",
"build": "rollup -c && tsc --emitDeclarationOnly",
},
"main": "./dist/パッケージ名.js", // CommonJS
"module": "dist/パッケージ名.esm.js", // ESModule
"types": "dist/types/index.d.ts", // 型定義ファイル
"author": {
"name": "ArakiTkaki",
"email": "arakitakaki.work@gmail.com"
},
"files": [ // 公開を許容するファイル(ホワイトリスト制) ほかにもnpmignore(ブラックリスト)などもあるが、僕はこちらのほうが最小publishが行えるため好き。
"dist",
"LICENCE"
]
}
パッケージの公開
初回だけ、publicにする必要があります
npm publish --access public
次回以降の公開タスク
$ npm run build
$ npm version patch # ここに関しては、Semverを参照してください。
$ git commit -m 'version up'
$ npm publish
$ git push
gitで、タグを切れば、タグのバージョンが搬入されるCI組んでも良さそうです。
まとめ
- rollupは出しわけが強い
- npmパッケージ公開は覚えてしまえば楽
- Semverの規約は開発者が困るのでしっかり守ろう。
- ライブラリ作成のためのBundlerのため、プラグインまわりが非常に強力
疑問点
Q Babelを使用する理由は? TypeScriptオンリーでやりたいんだけど。
A min.js(iife)を出力する際に必要です。
@rollup/plugin-typescript
を使用し、module
フィールドをESNEXT
とcommonjs
に変えてあげると大丈夫です。
import typescript from '@rollup/plugin-typescript';
import path from 'path';
// ...
export default [
{
plugins: [
typescript({
tsconfig: path.resolve('tsconfig.json'),
module: 'commonjs',
}),
]
// ... その他input outputなどの処理
},
{
plugins: [
typescript({
tsconfig: path.resolve('tsconfig.json'),
module: 'ESNEXT',
}),
]
// ... その他input outputなどの処理
}
]
Q Webpackで良いんじゃない?
A べつにどっちでも良いと思う。
- https://medium.com/webpack/webpack-and-rollup-the-same-but-different-a41ad427058c
- 英訳記事 https://postd.cc/webpack-and-rollup-the-same-but-different/
一方、Rollupが開発された背景は異なります。その目的は、優れたES2015のモジュールを活用して、一様に配布でき、できるだけ効率的なJavaScriptライブラリを作成可能にすることでした。webpackを含む他のモジュールバンドラの場合は、各モジュールを関数にラップする必要があります。また、それらをバンドルに含めるために各ブラウザに合ったrequireの実装をして、その1つ1つを評価しなければなりません。オンデマンドで読み込むものが必要な場合は問題ありませんが、そうでなければ無駄な作業になります。また、モジュール数が多いほど、大変なことになります。
ES2015のモジュールを使うと、別のアプローチが可能になります。その手法を採用しているのがRollupです。具体的には、全てのコードを同じ場所に置いて、一括で評価を行います。そのため、コードが簡潔でシンプルになり、起動速度が向上します。実際にRollupのREPLで、確認してみてください。
Q PeerDependenciesなどのパッケージを含めたくない
A https://github.com/pmowrer/rollup-plugin-peer-deps-external
何かとプラグインが多めなので、いろいろ探せばたいていのものは出てきます。
TODO // npm link を追記する