【Vite / Vue3】コンポーネントライブラリを自作してnpmに公開する手順

## 今回作成するコンポーネント
今回は、以下のようなシンプルなボタンのコンポーネントを作成しようと思います。
![Screen Shot 2023-03-07 at 19.52.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2356280/3e6ec3e6-0366-25a5-e709-f1b5d4c8d245.png)
以下のような`props`を設定し、ちょっとだけカスタマイズ可能なものにしようと思います。
| props      | 型 | 制限 | デフォルト値 |
| ----------- | ----------- | ----------- | ----------- |
| `btnText`      | String       | なし       | `Hello`       |
| `btnSize`   | String        | `large` / `medium` / `small` のみ許容        | `medium`        |

❗️今回作成するライブラリはVue3でのみ使用可能なものになります。Vue2で使用することはできません

## Viteを使用してVueプロジェクトを作成

今回は、Viteを使用してVue3アプリケーションを作成していきたいと思います。プロジェクトを作成する以下コマンドでアプリケーションを作成してください。対話形式のCLIが起動するので、それぞれ以下で指定された値を入力/選択してください。

~~~bash
npm create vite@latest

> ? Project name: › hello-button
> ? Select a framework: › Vue
> ? Select a variant: › JavaScript
~~~
作成したプロジェクトのディレクトリに移動し、`npm install`を実行します。
~~~bash
cd hello-button
npm install
~~~

今回はライブラリ作成用のアプリケーションの為実行しませんが、以下コマンド`npm run dev`を実行することでVite*Vue3の開発サーバーが起動します。
~~~bash
npm run dev

> vite-test-app@0.0.0 dev
> vite


  VITE v4.1.4  ready in 194 ms

  ➜  Local:   http://127.0.0.1:5173/ 👈ブラウザでここにアクセス

  ➜  Network: use --host to expose
  ➜  press h to show help
~~~
![Screen Shot 2023-03-10 at 4.50.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2356280/1de19e41-455e-b6f3-00de-96b205409dd3.png)

## ディレクトリ構成の修正
ライブラリを作成するにあたって、`src`ディレクトリ配下を修正していきます。
デフォルトでは以下のような構造になっていると思います。
~~~bash
├── src
│   ├── App.vue
│   ├── assets
│   │   └── vue.svg
│   ├── components
│   │   └── HelloWorld.vue
│   ├── main.js
│   └── style.css
~~~
以下のように修正してください👇
~~~bash
├── src
│   ├── components
│   │   └── HelloButton.vue
│   └── index.js
~~~

## コンポーネントを作成
scriptの定義には`CompositionAPI`を使用します。
props`btnSize`には`large`/`medium`/`small`のみ入り、渡された`btnSize`によって`width`を指定したクラスを変動させるという方法で、ボタンのサイズを可変にします。また、`large`/`medium`/`small`以外の値が渡された場合に警告をログに出力されるようにバリデーションの設定もしておきます。

~~~vue:src/components/HelloButton.vue
<template>
  <button
    class="hello-button"
    :class="`btn-${btnSize}`"
  >
    {{ btnText }}
  </button>
</template>

<script setup>
defineProps({
  btnText: {
    type: String,
    default: 'Hello'
  },
  btnSize: {
    type: String,
    default: 'medium',
    validator (value) {
      return ['large', 'medium', 'small'].includes(value);
    }
  },
});
</script>

<style scoped>
.hello-button {
  background: orange;
  color: black;
  border: none;
  outline: none;
  font-weight: bold;
}
.btn-large {
  width: 800px;
}
.btn-medium {
  width: 400px;
}
.btn-small {
  width: 200px;
}
</style>
~~~

## エントリーポイントを定義
ここでは、ライブラリとして使用される際のエントリーポイントとなるファイルを定義していきます。コンポーネントの登録を行う処理を持つ`install`メソッドをエクスポートしてください。
~~~javascript
import HelloButton from './components/HelloButton.vue';

export default {
  install (app, options = {}) {
    app.component('HelloButton', HelloButton);
  }
};
~~~


## Viteのビルド設定を定義
`vite.config.js`で、Viteを使用したビルドの設定を修正します。Viteではライブラリモードがサポートされている為、アプリケーションをライブラリとして使用する際のビルドのオプションを設定していきます。
これらの設定の詳細な説明に関しては、[Vite公式ドキュメント](https://ja.vitejs.dev/guide/build.html#%E3%83%A9%E3%82%A4%E3%83%95%E3%82%99%E3%83%A9%E3%83%AA%E3%83%A2%E3%83%BC%E3%83%88%E3%82%99)を参照してください。
~~~javascript:vite.config.js
import { defineConfig } from 'vite';
import { resolve } from 'path';
import vue from '@vitejs/plugin-vue';

// https://vitejs.dev/config/
export default defineConfig({
  build: {
    lib: {
      entry: resolve(__dirname, './src/index.js'),
      name: 'hello-button',
      fileName: (format) => `hello-button.${format}.js`
    },
    rollupOptions: {
      external: ['vue'],
      output: {
        globals: {
          vue: 'Vue'
        }
      }
    }
  },
  plugins: [vue()],
});

~~~

## ビルドの実行
以下コマンドを実行してビルドを実行します。
~~~bash
npm run build
~~~
以下のようなログが出力されればビルドが完了しています。
~~~bash
> first-hello-button@0.0.0 build
> vite build

vite v4.1.4 building for production...
✓ 4 modules transformed.
dist/style.css           0.22 kB │ gzip: 0.15 kB
dist/hello-button.es.js  0.78 kB │ gzip: 0.46 kB
dist/hello-button.umd.js  0.76 kB │ gzip: 0.48 kB
~~~
`dist`フォルダが追加され、バンドルされたファイルが出力されます。
~~~bash
├── dist
│   ├── hello-button.es.js
│   ├── hello-button.umd.js
│   └── style.css
~~~

## npmの設定を修正
`package.json`を編集してnpmの設定を更新します。
公開するファイル（`files`）、ライブラリとして使用される際のエントリーポイント（`main`/`module`/`exports`/）を設定していきます。
~~~javascript:package.json
{
  "name": "hello-button",
  "private": false, // falseに変更
  "version": "0.0.0",
  "type": "module",
  "main": "./dist/hello-button.umd.js", // 追加
  "module": "./dist/hello-button.es.js", // 追加
  "files": ["dist"], // 追加
  "exports": { // 追加
    ".": {
      "import": "./dist/hello-button.es.js",
      "require": "./dist/hello-button.umd.js"
    },
    "./dist/style.css": "./dist/style.css"
  },
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "vue": "^3.2.45"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^4.0.0",
    "vite": "^4.1.0"
  }
}
~~~

## ローカルでライブラリの動作確認
npmに公開する前に、ライブラリが正常に動作するかどうかをローカルで確認する必要があります。`npm link`コマンドを使用すると、ローカルでの動作確認を簡単に行うことができます。
`npm link`は、シンボリックリンクを作成するコマンドです。開発中のライブラリへのシンボリックリンクを作成し、それをローカルで立ち上げている別アプリから使用することで、実際に公開されているnpmを使用している状況を擬似的に再現することができます。
まずは、作成しているライブラリのディレクトリで以下コマンドを実行してください。
~~~bash
npm link
~~~
次に、別のVue3のアプリケーションの開発サーバーを起動してください。（テストに使用できるアプリケーションがない場合、本記事の`Viteを使用してVueプロジェクトを作成`、または[こちら](https://ja.vitejs.dev/guide/#%E6%9C%80%E5%88%9D%E3%81%AE-vite-%E3%83%95%E3%82%9A%E3%83%AD%E3%82%B7%E3%82%99%E3%82%A7%E3%82%AF%E3%83%88%E3%82%92%E7%94%9F%E6%88%90%E3%81%99%E3%82%8B)を参考にアプリケーションを起動してください）
そこで、以下コマンドを実行してください。
~~~bash
npm link hello-button
~~~
`node_modules/`配下に`hello-button`というディレクトリが新しく追加されます。
また、`ls`コマンド等で`node_modules/hello-button`を確認すると、以下のように作成しているライブラリのディレクトリを参照していることが確認できます。
~~~javascript
hello-button -> ../../hello-button
~~~
それではライブラリを使用していきましょう。テスト用アプリの`src/main.js`を以下のように編集してください。
~~~javascript:src/main.js
import { createApp } from 'vue';
import './style.css';
import App from './App.vue';

import HelloButton from 'hello-button'; // 追加
import 'hello-button/dist/style.css'; // 追加

createApp(App).use(HelloButton).mount('#app'); // use()を追加
~~~
ここで追加した`use()`が実行されたタイミングで、対象のライブラリの`package.json`で指定したエントリーポイント`src/index.js`の`install()`メソッドが実行され、アプリケーションにライブラリのコンポーネントが登録されているというイメージです。
では、コンポーネントの登録が完了したので、実際に使っていきましょう。
今回はグローバルコンポーネントとして登録しました。テスト用アプリのどのコンポーネントでも良いので、以下タグを追加してみてください。
~~~vue
<hello-button :btnSize="`large`" :btnText="`Hello World!!`"></hello-button>
~~~
以下のようにブラウザに作成したライブラリのボタンが表示されていれば、ライブラリが問題なく動作することが確認できます！
![Screen Shot 2023-03-10 at 6.13.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2356280/b70a5614-ed68-8361-390c-bbb8b2a8c2af.png)


## npmに公開
ローカルでライブラリが正常に動作することが確認できたので、実際にnpmに公開していきましょう。
`npm publish`コマンドを実行することで、作成したライブラリを公開することができます。以下コマンドを実行して、処理が正常終了すればライブラリの公開が完了です！！
~~~php
npm publish
.
.
.
+ first-hello-button@0.0.0 // このような出力があれば正常終了です
~~~
試しに、[npmのサイト](https://www.npmjs.com/)でパッケージの検索をしてみましょう。検索バーに公開したライブラリの名前（`package.json`>`name`の値）を入力し、検索してみてください。以下のように公開したパッケージが検索結果に表示されることが確認できるかと思います✨記念すべき瞬間です。
![Screen Shot 2023-03-10 at 6.44.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2356280/74d46950-c93f-409d-3ab3-56911aeef8de.png)

#### `npm publish`を実行する際によく出るエラー①：未認証エラー
~~~php
npm ERR! code ENEEDAUTH
npm ERR! need auth This command requires you to be logged in to https://registry.npmjs.org/
npm ERR! need auth You need to authorize this machine using `npm adduser`
~~~

👉 パッケージを公開する場合、npmにログインしている必要があります。npmにアカウントがない場合は登録し、以下コマンド（`npm login`）を実行してnpmへのログインを完了させてください。
~~~php
npm login
> Username: // ユーザーネームを入力
> Password: // パスワードを入力
> Email: (this IS public): // 登録しているメールアドレスを入力
npm notice Please check your email for a one-time password (OTP)
> Enter one-time password: // 入力したメールアドレス宛てにOTPが送信されるので、確認して入力
Logged in as {ユーザーネーム} on https://registry.npmjs.org/. // これが出力されればログイン完了です！
~~~
ログインしているユーザーを確認する以下コマンド`npm whoami`を実行し、実行結果にログインしたユーザー名が出力されれば、正常にログインができています。
~~~bash
npm whoami
~~~

#### `npm publish`を実行する際によく出るエラー②：公開範囲エラー
~~~php
npm ERR! code EPRIVATE
npm ERR! This package has been marked as private
npm ERR! Remove the 'private' field from the package.json to publish it.
~~~
👉 パッケージのプライベートモードが有効になっている際に出るエラーです。`package.json`の`private`が`true`になっている場合、プライベートモードが有効になる為、npmへの公開ができません。こちらを`false`に変更することでエラーが解消します。
~~~bash:package.json
"private": true
👇
"private": false
~~~

#### `npm publish`を実行する際によく出るエラー③：パッケージ名重複エラー
~~~php
npm ERR! 403 403 Forbidden - PUT https://registry.npmjs.org/hello-button - Package name too similar to existing package hellobutton; try renaming your package to '@whopper1962/hello-button' and publishing with 'npm publish --access=public' instead
npm ERR! 403 In most cases, you or one of your dependencies are requesting
npm ERR! 403 a package version that is forbidden by your security policy, or
npm ERR! 403 on a server you do not have access to.
~~~
👉 公開しようとしているパッケージの名前が、既に公開されているパッケージと同じまたは似ている場合は上記のエラーが出ます。`package.json`の`name`がパッケージの名前として使用されるので、他のパッケージと被らないような名前に変更することでエラーが解消します。パッケージ名にユーザーネーム等を入れると被りにくいです。
~~~bash:package.json
"name": "hello-button",
👇
"name": "{ユーザーネーム}-hello-button",
~~~



### 実際にインストールして動作確認...完成！🔥

では、先ほど使用したテストアプリの環境で、ローカルで作成したシンボリックリンクではなく実際に公開したものをインストールする形でライブラリの動作確認を行っていきましょう！
テストアプリ環境で、以下コマンドを実行してください。これで導入したシンボリックリンクを削除することができます。（実行後、`node_modules/`配下から`hello-button`が消えていることが確認できるかと思います）
~~~bash
npm install
~~~
ここで、テストアプリの開発サーバーを再起動してみてください。以下のようなエラーが出力されると思います。
![Screen Shot 2023-03-10 at 6.42.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2356280/0d6e9990-0e3e-078a-3f8c-9fa5480b0408.png)
それではインストールしていきましょう。npmで、作成したライブラリのページに移動し、画面右の`Install`のエリアをクリックしてください。インストール用のコマンドがクリップボードにコピーされます。
![Screen Shot 2023-03-10 at 6.43.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2356280/7f0465c1-57e9-dee5-a79a-229bb4cf0e30.png)
インストールコマンドを実行し、正常にインストールが完了したら、テストアプリ環境をもう一度見てみてください。（必要に応じて`src/main.js`の`import`部分のパッケージ名を修正してください）
![Screen Shot 2023-03-10 at 6.13.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2356280/b70a5614-ed68-8361-390c-bbb8b2a8c2af.png)
問題なくボタンが表示されることが確認できれば、コンポーネントライブラリの完成です！おめでとうございます！！👏👏👏

![giphy.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2356280/dee45a12-809e-1587-ebed-a430b81ca25c.gif)
最後まで読んでいただき、ありがとうございました！