中規模npmライブラリ開発入門 - マイクロレポジトリ編


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`が使用できない
- CDNサーバーを公開して、browserに直接ライブラリ提供を行いたい

上記を柔軟に対応するため、出し分けを行えるように作成したほうが利用者に対し、やさしいためです。

当初`lodash`は、`CommonJS`のみに対応していた（と思う）ため、`tree shaking`などが行えない、などの弊害がありましたが
のちに`lodash-es`が開発されたという事もあり、両方の対応（特にESModule）の対応は結構大事だと思います。

## 使用するライブラリ

- typescript
- babel
- rollup

## マイクロレポジトリとは

1レポジトリに対して、1ライブラリを管理する手法です。

### 良い部分

- 同一レポジトリの依存関係が無く（そりゃそう）見通しが良い
- 基本的にすべてのnpmコマンドは1レポジトリを起点に作成されているため、コマンドをスクリプトで記述する必要がない

### 悪い部分

- パッケージを分けたい場合、柔軟ではない
    - 例) `react-router`と`react-router-dom`のような分け方をする際適していない
        - https://github.com/ReactTraining/react-router
    - 例) https://github.com/OnsenUI/OnsenUI のように forReact forVue などコアライブラリに対して派生させるのには向いていない
        - 上記に関しては別レポジトリに分けても良いとは思ってるため議論するべき項目


## 今回なぜ`rollup`を使用するのか

場合分けアウトプットが非常に楽になるためです。

利用者は下記の3種類考えられると思っています

- Broserからscriptタグを直接使用したい
- `import / export` 構文のモジュールを使用して `Tree Shaking`を使用したい
- `commonjs`を使用し、`require('hogehoge')`という風に記述したい

webapckでのだし分け

https://webpack.js.org/configuration/output/

rollupでのだし分け

https://rollupjs.org/guide/en/#configuration-files

rollupの場合、非常に簡素に、出し分けが行えます。

# バージョニングに関して

NPMパッケージを利用する際にパッケージのバージョンは結構気にすると思いますが、こちらにも規則があります。

NPMは[semver](https://semver.org/)という手法を用いてバージョン管理が行われております。

- [semver.org](https://semver.org/)
- [semver npm package](https://www.npmjs.com/package/semver)

## 概要

`"@babel/plugin-proposal-class-properties": "7.8.3",`
こちらを参考に分解するのであれば

Majorは7 Minorは8 Patchは3 となります。

## Major Version

***後方互換を伴わない破壊的なアップデート***が行われる際などはこちらを使用します。
- 機能の削除
- 引数の変更

注意として、割と有名なライブラリでも、このsemverの規則を守らないものがあったりなんやり。

[Backbone.js - Follow SemVer #2888](https://github.com/jashkenas/backbone/issues/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)

```ts:rollup.config.js
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/**/*'],
    })
  ],
};
```

```tsconfig.json
{
  "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
// .babelrc
{
  "presets": [
    "@babel/preset-env",
    "@babel/preset-typescript"
  ],
  "plugins": [
    "@babel/proposal-class-properties",
    "@babel/proposal-object-rest-spread"
  ]
}

```

## NPMへの公開タスク

ターミナルから会員登録が可能です

```sh
$ 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を追加しよう

[npmへログインします](https://www.npmjs.com/)

![asfddasffdasfdas.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199120/e22e864e-4dc9-c6aa-aa08-34c2f0f95a4c.png)

![asfddasfsfdafdasfd.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/199120/ff421f0e-d517-74ff-d65d-a2e4d1c14d2a.png)

## package.jsonの編集

```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`


## 次回以降の公開タスク

```sh
$ 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`に変えてあげると大丈夫です。

```js:rollup.config.js
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 を追記する