ロカオプ技術ブログ

株式会社ロカオプの技術ブログです。

React npm ライブラリ/パッケージの作成入門からnpm公開まで

React npm ライブラリ/パッケージの作成入門からnpm公開まで(RollupとTypeScript編)

こんにちは、皆さん!今日は、RollupとTypeScriptを使用してReactのnpmパッケージを作成し、exampleディレクトリで動作確認する方法について詳しく説明します。この記事では、初心者でも分かりやすいようにステップバイステップで進めていきます。さっそく始めましょう!

作ったもの

react-iconsを同梱したアイコンピッカーです。

www.npmjs.com

github.com

locaop-owner.github.io

locaop.jp

なぜ自作のライブラリを作ったか

色々とnpmにはアイコンピッカーが存在するのもあり、その中のひとつを使用してアプリケーションに組み込んでいましたがそのライブラリが開発も終了?しているような感じだったので依存しているreact-iconsが古いままだったのでSNSやその他使用したいアイコンを追加するなどが出来ませんでした。そのため互換性を保ちつつアイコンを追加できるようにそのライブラリをフォークして再開発を行いました。 結果、同梱するアイコンも自由に加減ができるようになった、というのが今回のきっかけになります。

1. 前提条件

必要なツール
  • Node.jsとnpmがインストールされていること。
  • 基本的なReactとTypeScriptの知識。

2. プロジェクトのセットアップ

まず、新しいディレクトリを作成し、その中でnpmプロジェクトを初期化します。

mkdir my-react-component
cd my-react-component
npm init -y

次に、ReactとReactDOM、そして開発に必要な依存関係をインストールします。

npm install react react-dom react-icons tslib
npm install --save-dev @rollup/plugin-commonjs @rollup/plugin-node-resolve @rollup/plugin-typescript rollup rollup-plugin-peer-deps-external rollup-plugin-terser typescript @types/react @types/react-dom

ライブラリにはどのようにパッケージするの?(npm rollupもしくはnpm pack?)

ここでまずライブラリを作る上で重要なものがあります。それがrollupです。 最初は会社内だけで使用するからnpm packというコマンドでパッケージングしてファイルをどこかにおいて それをnpm installすればいいのでは?というのも考えました。

npm packとは

pack は、通常 npm pack として使用され、npmパッケージをアーカイブ化して .tgz ファイル(tarball)にします。このファイルは後に npm install でインストール可能です。

  • パッケージをアーカイブして、後でインストール可能な形式に変換します。
  • 通常、npmパッケージの配布や公開前のテストなどで使用されます。

pack はnpmパッケージをアーカイブ化し、後でインストール可能な形式に変換するために使用されます。

docs.npmjs.com

qiita.com

rollupとは

rollup は、JavaScriptのモジュールバンドラーです。主にESモジュールを対象にしており、Tree-shaking(不要なコードの削除)などを行い、効率的にバンドルを生成します。

  • ESモジュールに特化しており、Tree-shakingによって最適化されたバンドルを生成できる。
  • 主にライブラリやパッケージの開発で使用され、特にフロントエンド向けのパッケージングに適している。

rollup はモジュールバンドラーで、JavaScriptのコードを効率的にパッケージングするために使用されます。

rollupjs.org

www.npmjs.com

devhints.io

使い方によって用途は分かれる

今回は当初パッケージングして使用を考えていましたがgithubとnpmに登録して 公開することでOSSでもあり今後もアップデートの際にバージョン変更するだけで 様々なものに反映できるということでrollupにしました。

コマンドは

  • -c(configファイル(rollup.config.js)にもとづいてrollupする)
  • -w(Watchモード)

を使用してます。

rollupjs.org

ルート直下のrollup.config.jsでrollupの設定が出来ます。 ここらへんは上記のページを参考に行いました。またビルドされて 配布用には軽くするためにソースマップは不要にしました。

3. ディレクトリ構造の設定

プロジェクトのディレクトリ構造を以下のように設定します。

my-react-component/
├── dist/
├── example/
│   ├── src/
│   │   ├── App.tsx
│   │   └── index.tsx
│   ├── public/
│   │   └── index.html
│   └── package.json
├── src/
│   ├── index.ts
│   └── components/
│       ├── IconPicker.tsx
│       └── index.tsx
├── .gitignore
├── package.json
├── rollup.config.js
└── tsconfig.json

4. TypeScriptの設定

TypeScript設定ファイルの作成

プロジェクトのルートにtsconfig.jsonファイルを作成し、以下の内容を追加します。 ここに関しましてはtsconfigの内容なので割愛します。

{
  "compilerOptions": {
    "importHelpers": true,
    "noEmitHelpers": true,
    "target": "ES5",
    "module": "ESNext",
    "declaration": true,
    "declarationDir": "dist/types",
    "outDir": "dist",
    "jsx": "react",
    "strict": true,
    "moduleResolution": "node",
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src"]
}

5. Rollupの設定

Rollup設定ファイルの作成

プロジェクトのルートにrollup.config.jsファイルを作成し、以下の内容を追加します。 先ほどの説明の通りデプロイ時に軽くするためソースマップを無効にしています。

その他に関してはrollupの公式にあるものや下記を参考にしました。

zenn.dev

qiita.com

import resolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import peerDepsExternal from "rollup-plugin-peer-deps-external";
import typescript from "@rollup/plugin-typescript";
import { terser } from "rollup-plugin-terser";
import pkg from "./package.json";

export default {
  input: "src/index.ts",
  output: [
    {
      file: pkg.main,
      format: "cjs",
      sourcemap: false, // ソースマップを無効にする
    },
    {
      file: pkg.module,
      format: "es",
      sourcemap: false, // ソースマップを無効にする
    },
  ],
  plugins: [
    peerDepsExternal(),
    resolve(),
    commonjs(),
    typescript({ tsconfig: "./tsconfig.json" }),
    terser(),
  ],
  external: ["react", "react-dom", "@emotion/react"], // ReactとReactDOM、Emotionを外部依存として指定
};

簡単に説明すると

  • input: "src/index.ts": バンドルのエントリーポイントを指定します。
  • output: 出力設定を指定します。ここではCommonJS形式とESモジュール形式の2種類を出力しています。ソースマップは無効に設定されています。
  • plugins: 使用するプラグインを指定します。
  • peerDepsExternal(): 外部依存を外部に設定します。
  • resolve(): Node.jsのモジュール解決をRollupで行えるようにします。
  • commonjs(): CommonJSモジュールをESモジュールに変換します。
  • typescript({ tsconfig: "./tsconfig.json" }): TypeScriptをバンドルに使用します。
  • terser(): コードを圧縮します。
  • external: バンドルに含めない外部依存を指定します。この場合、React、ReactDOM、Emotionが指定されています。

6. コンポーネントの作成

srcディレクトリ内のcomponensディレクトリ内にindex.tsxを作成し、以下のコードを追加します。

export * from './IconPicker';

srcディレクトリ内直下にindex.tsを作成し、以下のコードを追加します。

export { IconPicker } from './components/IconPicker';

srcディレクトリ内のcomponensディレクトリ内のIconPicker.tsxにはライブラリの開発コードが入ります。

7. パッケージのビルド

package.jsonに以下のスクリプトを追加します。

"scripts": {
  "build": "rollup -c",
  "watch": "rollup -c --watch",
  "lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'",
  "format": "prettier --write 'src/**/*.{js,jsx,ts,tsx,json,css,scss,md}'"
}

このスクリプトを実行すると、srcディレクトリ内のコードがdistディレクトリにビルドされます。

npm run build

8. Exampleアプリでの動作確認

Exampleディレクトリのセットアップ

exampleディレクトリにsrcフォルダとpublicフォルダを作成し、それぞれに必要なファイルを作成します。

mkdir -p example/src/public/

example/src/App.tsxに以下のコードを追加します。

import React from 'react';
import ReactDOM from 'react-dom';
import MyButton from '@locaop/icon-picker';

const App = () => {
  return (
    <div>
      <MyButton label="Click Me" onClick={() => alert('Button Clicked!')} />
    </div>
  );
};

ReactDOM.render(<App />, document.getElementById('root'));

example/public/index.htmlに以下のコードを追加します。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>React Component Example</title>
</head>
<body>
  <div id="root"></div>
</body>
</html>
Exampleアプリの設定

example/package.jsonを作成し、以下の内容を追加します。

{
  "name": "example",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@emotion/react": "^11.4.0",
    "@locaop/icon-picker": "file:../",
    "react": "file:../node_modules/react",
    "react-dom": "file:../node_modules/react-dom",
    "react-scripts": "4.0.3"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
    "predeploy": "npm run build",
    "deploy": "gh-pages -d build"
  },
  "homepage": "https://locaop-owner.github.io/locaop-react-icons-picker/",
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  },
  "devDependencies": {
    "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
    "gh-pages": "^6.1.1"
  }
}
ファイルのパスの部分が重要

なにか上記のpackage.jsonで不思議な場所はありませんでしたか? そう。ここです。

    "react": "file:../node_modules/react",
    "react-dom": "file:../node_modules/react-dom",

なぜルートのsrcの方(開発している方)の相対パスを入れているのでしょうか。 これはreact本体の中で別のreact本体を動作するものを防止するためです。

エラーが出てexampleが動作しないです。

> Uncaught Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
> 1. You might have mismatching versions of React and the renderer (such as React DOM)
> 2. You might be breaking the Rules of Hooks
> 3. You might have more than one copy of React in the same app
> See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.
>     at resolveDispatcher (react.development.js:1465)
>     at Object.useState (react.development.js:1496)
>     at ../dist/PlainJsonEditor.js.exports.PlainJsonEditor (PlainJsonEditor.js:38)
>     at renderWithHooks (react-dom.development.js:14803)
>     at mountIndeterminateComponent (react-dom.development.js:17482)
>     at beginWork (react-dom.development.js:18596)
>     at HTMLUnknownElement.callCallback (react-dom.development.js:188)
>     at Object.invokeGuardedCallbackDev (react-dom.development.js:237)
>     at invokeGuardedCallback (react-dom.development.js:292)
>     at beginWork$1 (react-dom.development.js:23203)

3. You might have more than one copy of React in the same app (意訳)Reactが複数含まれているかもしれません

そのために配布する際はpeerDependenciesが存在します。

peer 依存関係は特別な種類で、自身のパッケージを公開したい場合のみ扱うものです。

peer 依存関係を持つことは、あなたのパッケージがパッケージをインストールする人と全く同じ依存関係を必要とするということです。 react のように、それをインストールした人からも使用される、単一の react-dom のコピーを持つ必要があるパッケージで役に立ちます。

chore-update--yarnpkg.netlify.app

qiita.com

qiita.com

つまりexampleのreactと依存関係をなくすためには同一の上記のreactとreact-domを使用する必要があります。 しかしローカル環境では解消することはできません。

なぜなら peerDependencies は yarn install (または npm install )に無視されるので、開発時にビルドできなくなるからです

qiita.com

解消するには

  1. npm linkを使う
  2. 依存パッケージは相対パスで使う

が存在しますがnpm linkはうまく動作しないので2の相対パスで今回は行いました。

docs.npmjs.com

zenn.dev

次に、必要なパッケージをインストールします。

cd example
npm install

9. Exampleアプリの実行

Exampleアプリを実行して、パッケージの動作を確認します。

npm start

ブラウザでhttp://localhost:3000にアクセスし、ボタンが正しく表示されるか確認します。

10. パッケージの公開準備

プロジェクトのルートのpackage.jsonに以下の設定を追加します。

{
  "name": "@locaop/icon-picker",
  "version": "0.4.1",
  "description": "This is an icon picker for react-icons.",
  "main": "dist/index.js",
  "module": "dist/index.es.js",
  "types": "dist/types/index.d.ts",
  "publishConfig": {
    "access": "public"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/locaop-owner/locaop-react-icons-picker"
  },
  "homepage": "https://locaop.jp",
  "scripts": {
    "build": "rollup -c",
    "watch": "rollup -c --watch",
    "lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'",
    "format": "prettier --write 'src/**/*.{js,jsx,ts,tsx,json,css,scss,md}'"
  },
  "keywords": [
    "react",
    "icon",
    "picker",
    "icon-picker",
    "icon-picker-react",
    "icon-picker-react-component"
  ],
  "author": "locaop",
  "license": "ISC",
  "devDependencies": {
    "@rollup/plugin-commonjs": "^25.0.7",
    "@rollup/plugin-node-resolve": "^15.2.3",
    "@rollup/plugin-typescript": "^11.1.6",
    "@types/react": "^17.0.1",
    "@types/react-dom": "^17.0.1",
    "rollup": "^2.79.1",
    "rollup-plugin-peer-deps-external": "^2.2.4",
    "rollup-plugin-terser": "^7.0.2",
    "typescript": "^5.4.5"
  },
  "dependencies": {
    "old-react-icons": "npm:react-icons@^3.11.0",
    "react-icons": "^5.2.1",
    "tslib": "^2.6.2"
  },
  "peerDependencies": {
    "@emotion/react": "^11.4.0",
    "react": "^17.0.1",
    "react-dom": "^17.0.1"
  }
}

11. npmへの公開

あとはroll upしたパッケージをルートから公開コマンドをするだけです。

docs.npmjs.com

deku.posstree.com

まず、npmにログインします。

npmで自分が作ったライブラリを配布するためにはアカウントが必要です。 npmのアカウントがない場合は、下記のリンクをクリックしてアカウントを新規登録してください。 npmサイト:

www.npmjs.com

npm login

次に、パッケージを公開します。

npm publish

以上で、Reactのnpmパッケージが公開されました!

まとめ

この記事では、RollupとTypeScriptを使用してReactのnpmパッケージを作成し、exampleディレクトリで動作確認するための手順を説明しました。これで、自分の作成したReactコンポーネントを簡単に共有し、動作を確認できるようになります。ぜひ試してみてください!

コメントや質問があれば、下記にお願いします。フィードバックをお待ちしています!