LoginSignup
8
3

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-05-31

https://github.com/ArakiTakaki/qiita-example-npm-microrepo
サンプルレポジトリ

概要

案件特化のライブラリを制作したくなるケース、存在しませんか?
単一ファイルの場合は、1ファイルで終わりですが、必ずしも単一ファイルで終わりなわけではありません。
対応するにも懸念点が多かったりもします。

  • browserへの対応 hogehoge.min.js
  • commonjsへの対応 hogehoeg.cjs.js
  • ESModuleへの対応 hogehoge.es.js

Webpackの場合、これらのだし分けが苦労したため、今回は、Rollupというバンドラーを用い、ライブラリの基礎を作成していくことにしました。

今回使用するRollupReactbabelで使用されており、設定が非常に軽微であるためこれを使用しました。

他にも下記のライブラリがRollupで組まれている

  • Vue
  • Ember
  • Preact
  • D3
  • Three.js
  • Moment

なぜ出し分けが必要なのか

  • NodeJSでサラッとアプリを作る際にimportexport構文が邪魔になる。
  • TypeScriptを使用している際に、commonjsでないとimportできない
    • CommonJSだとtree shakingが使用できない
  • CDNサーバーを公開して、browserに直接ライブラリ提供を行いたい

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

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

使用するライブラリ

  • typescript
  • babel
  • rollup

マイクロレポジトリとは

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

良い部分

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

悪い部分

  • パッケージを分けたい場合、柔軟ではない
    • 例) react-routerreact-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)

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
{
  "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を追加しよう

npmへログインします

asfddasffdasfdas.PNG

asfddasfsfdafdasfd.PNG

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フィールドをESNEXTcommonjsに変えてあげると大丈夫です。

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 べつにどっちでも良いと思う。

一方、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 を追記する

8
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
3