📦

Vite で TypeScript + React のコンポーネントライブラリを作成してみる

2024/02/26に公開

はじめに

Vite のライブラリモードを使ってnpm installできるコンポーネントライブラリを作ってみたかったのでやってみた。コンポーネントを作成する時に Storybook を利用した形の記事が日本語記事で発見できなかったので記事を書いてみることにした。ライブラリを作ってみたことがない中、ChatGPT に教わりつつ検索をしながら進めていったので間違っている場所があるかもしれないが、その際は指摘してくれるとありがたい。

なお、この記事は下記のテンプレートを参考にした。一度中身を覗いてみると良い。

https://github.com/IgnacioNMiranda/vite-component-library-template

作ってみるもの

極めてシンプルなボタンコンポーネントだけを用意したライブラリを作ってみる。

自作ボタンコンポーネント
今回作るボタンコンポーネント(画像は Storybook 上)

ローカル環境の準備

開発環境として Node.js が必要だが、それを直接インストールするのではなく node のバージョン管理ツールであるvoltaを導入しておく。特に様々なプロジェクトを PC 内に同居させているとバージョン管理が後々大変になるので導入しておくと良い。
https://docs.volta.sh/guide/getting-started

React のプロジェクト準備

ここからどこかにフォルダを作ってそこにプロジェクトを作成するが、実際には GitHub などでリポジトリを作ってgit cloneしてきてから行った方が良い。

プロジェクトの作成

TypeScript + React + Vite のプロジェクトを作成する。ありがたいことに Vite にはそれを作成する公式コマンドが用意されているのでそれを使う。どこかに今回のプロジェクトの起点とあるフォルダを作成し、そのフォルダの中で以下のようなコマンドを叩く。

npm create vite@latest . -- --template react-swc-ts
npm install

場合によっては対話モードが開くかもしれないが、プロジェクト名をカレントディレクトリ.に指定し、React → TypeScript + SWC を選べば良い。

Storybook の導入

以下のコマンドを叩くと Storybook の導入ができる。

npx sb init --builder=vite

これによって Storybook のテンプレートが生成され、.storybooksrc/storiesフォルダが追加されるはずである。そして自動的に Storybook の画面が表示されるだろう。もし Storybook を触ったことがない人はここで少し触れてみると良い。

ライブラリ用にファイルを再構成

これで必要なものは導入できたので、次は以下のように構成を変える。npm create viteで作成されたpublicフォルダやindex.html等を削除し、srcの中身はvite-env.d.tsのみ残して大胆に全部消した。

$ tree -a -I "node_modules|.git"
.
├── .eslintrc.cjs
├── .gitignore
├── .storybook
│   ├── main.ts
│   └── preview.ts
├── README.md
├── package-lock.json
├── package.json
├── src
│   ├── components
│   │   ├── Button
│   │   │   ├── Button.stories.tsx
│   │   │   ├── Button.tsx
│   │   │   └── index.ts
│   │   └── index.ts
│   ├── index.ts
│   └── vite-env.d.ts
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts

今回はコンポーネントを Storybook 上で開発したいので、以下のようにpackage.jsonを書き換える。

package.json
  {
    ...
    "scripts": {
+     "dev": "storybook dev -p 6006",
-     "dev": "vite",
      "build": "tsc && vite build",
      "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
-     "preview": "vite preview",
-     "storybook": "storybook dev -p 6006",
-     "build-storybook": "storybook build"
    },
    ...
  }

これで以下のコマンドから Storybook が開くようになった。

npm run dev

コンポーネントライブラリの作成

Button コンポーネントの作成

今回用意するのはとてもシンプルなボタンコンポーネントである。(ChatGPT3.5 くんに書いてもらった。)

src/components/Button/Button.tsx
import React from "react";

export type ButtonProps = {
  label: string;
  onClick?: () => void;
};

export const Button: React.FC<ButtonProps> = ({ label, onClick }) => {
  return <button onClick={onClick}>{label}</button>;
};

Story の用意

stories は以下のように用意した。なお Storybook は v7 を使用しており、記法は CSF3 を利用している。

src/components/Button/Button.stories.tsx
import { Meta, StoryObj } from "@storybook/react";
import { Button } from "./Button";

const meta: Meta<typeof Button> = {
  title: "Components/Button", // 省略可
  component: Button,
};

export default meta;

export const Default: StoryObj<typeof Button> = {
  args: {
    label: "Click me",
  },
};

export const WithOnClick: StoryObj<typeof Button> = {
  args: {
    label: "Click me with onClick",
    onClick: () => alert("Button clicked!"),
  },
};

これで一度npm run devで Storybook を開いて確認してみよう。http://localhost:6006を見に行くとButtonが追加されて、触れるようになっているはずだ。

コンポーネントの export

今の状態ではコンポーネントを外部から使うことができない。exportを利用して外部に公開できるよう、各フォルダのindex.tsexport文を追加していく。

src/components/Button/index.ts
export * from "./Button" // Button.tsx ファイル
src/components/index.ts
export * from "./Button" // Button フォルダ
src/index.ts
export * from "./components"

これで、npm installした側で以下のように記述すれば、Buttonコンポーネントを利用できるようになる。

import { Button } from "<パッケージ名>";

これでライブラリ自体は用意できた。

デプロイの準備

パッケージとして公開するためには様々な設定が必要になる。まずはvite.config.tsを以下のように設定する。

vite.config.ts
  import { defineConfig } from 'vite'
  import react from '@vitejs/plugin-react-swc'

  export default defineConfig({
    plugins: [react()],
+   build: {
+     outDir: 'dist', // default の設定と同じ
+     lib: {
+       entry: 'src/index.ts',
+       name: '<パッケージ名>',
+       fileName: 'index',
+       formats: ['es', 'umd'], // default の設定と同じ
+     },
+     rollupOptions: {
+       external: ['react', 'react-dom'],
+       output: {
+         globals: {
+           react: 'React',
+           'react-dom': 'ReactDOM',
+         },
+       },
+     },
+   },
  });

build.rollupOptionsにある設定はVite 公式の説明にもあるようなライブラリにバンドルしたくない依存関係に関するものである。react等はこのライブラリを使うときに入っている前提であるので、これらはバンドルしないように設定した方が良い。これに関して追加でpackage.jsonに以下の記述を追加する。

package.json
  {
    ...
+   "peerDependencies": {
+     "react": "18.2.0",
+     "react-dom": "18.2.0"
+   },
    ...
  }

peerDependenciesとはこのライブラリをnpm installする際に、すでにinstallされているものとして取り扱う依存先を記述するためのものである。その挙動はnpmのバージョンによって挙動が異なるので注意が必要である。

https://qiita.com/masato_makino/items/dafb63982e6f20186122

次にtsconfig.jsonでは以下のように付け足す。

tsconfig.json
  {
    "compilerOptions": {
      ...
      /* Bundler mode */
      ...
+     "emitDeclarationOnly": true,
+     "declaration": true,
+     "declarationMap": true,
+     "declarationDir": "dist",
-     "noEmit": true,
      ...
    },
    "include": ["src"],
+   "exclude": ["**/*.stories.*"],
    ...
  }

まず、compilerOptionsdeclarationに関係する記述だが、TypeScript の型情報をバンドルするための設定である。初期設定のままだと型情報はバンドルされない。なぜならば、多くのプロジェクトはライブラリではなく Web サービスの構築であるので、その運用に型情報は必要ないからである。
次にexcludeで Storybook に関係するスクリプトをライブラリにバンドルしない設定である。これに加えて例えば**/*.mdxVitest などのテストツールを使っているのであれば**/*.spec.***/*.test.*もライブラリ本体には必要ないのでこの項目に入れておくと良いだろう。

最後にpackage.jsonのビルドのタスクランナーを少し調整する。

package.json
  {
    ...
    "type": "module",
    ...
    "scripts": {
      "dev": "storybook dev -p 6006",
+     "build": "vite build && tsc",
-     "build": "tsc && vite build",
      "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
    },
    ...
  }

tscコマンドとvite buildコマンドの順番を入れ替えるだけである。これはvite buildコマンドがビルドファイルの出力先であるdistフォルダをクリーンにしてしまうので、tscで生成された型情報ファイル(これもtsconfig.jsondeclarationDirdistフォルダに入れるよう設定している)が消し飛んでしまうことを避けるためである。

一度これでnpm run buildを実行してみると、上記と同じ設定をしているのであれば以下のようなdistフォルダが作成される。

$ tree dist
dist
├── components
│   ├── Button
│   │   ├── Button.d.ts
│   │   ├── Button.d.ts.map
│   │   ├── index.d.ts
│   │   └── index.d.ts.map
│   ├── index.d.ts
│   └── index.d.ts.map
├── index.d.ts
├── index.d.ts.map
├── index.js
└── index.umd.cjs

index.jsが ESModule でindex.umd.cjsが UMD のファイルである。index.d.tsは TypeScript のための型定義ファイルになっている。これらのファイルパスをpackage.jsonに記述する。

package.json
  {
    ...
    "name": "<パッケージ名>",
-   "private": true,
+   "description": "<説明>",
+   "author": {"name": "<製作者名>"},
+   "version": "0.1.0",
-   "version": "0.0.0",
    "type": "module",
+   "main": "./dist/index.umd.cjs",
+   "module": "./dist/index.js",
+   "types": "./dist/index.d.ts",
+   "exports": {
+     ".": {
+       "types": "./dist/index.d.ts",
+       "import": "./dist/index.js",
+       "require": "./dist/index.umd.cjs"
+     }
+   },
+   "files": [
+     "dist"
+   ]
    ...
  }

"private": trueの項目が残っているとnpm publishが実行できなくなるので注意が必要である。

デプロイおよびインストール・・・は別記事で

さて、デプロイの準備が整ったが、実際のデプロイとインストールのテストについてはどのサービスにデプロイするかによって異なる。
デプロイ先については npm レジストリに行うのが最も楽だがこれは public なレジストリ[1]である。private に公開したい場合は GitHub Packages などのレジストリを使うと良いだろう。
GitHub Packages と Azure Artifacts については別の記事として書いたので参考にしてもらいたい。

https://zenn.dev/takanari_dev/articles/2024-02-23-npm-github-packages
https://zenn.dev/takanari_dev/articles/2024-02-23-npm-azure-artifacts

記事全体としての参考サイト

https://zenn.dev/drop_table_user/articles/7b043bef6cec29
https://zenn.dev/s_takashi/articles/20ecebd0a42010

脚注
  1. npm でも 有料ユーザーであれば private な公開が可能らしい(https://docs.npmjs.com/creating-and-publishing-private-packages) ↩︎

Discussion