[Vue.js] オリジナルコンポーネントライブラリを作って公開しよう!

オリジナルのコンポーネントライブラリを作って公開するまでの手順です。

著者も試行錯誤しながらとりあえず公開できるところまでもっていった段階ですので、至らない点があると思います。
不備を見つけられた方はコメントで教えてくださると幸いです。

完成品は[こちら](https://github.com/isuke/my-components-sample)です。

# 前提

`node`はもちろん、`yarn`も使いますので入れておいてください。
`yarn`を使わない方は適宜、`npm`で読み替えてください。

# コンポーネントを作る

まずはディレクリを作って、

```shell-session
$ mkdir my-components-sample
$ cd my-components-sample
```
`yarn init`して`package.json`を作ります。
`private`を`false`にするのがポイントです。`true`だと公開できないですからね。
`main`は後で書き換えるので適当でいいです。

```shell-session
$ yarn init      
yarn init v1.3.2
question name (my-components-sample): 
question version (1.0.0): 
question description: My Component Sample
question entry point (index.js): 
question repository url: 
question author: 
question license (MIT): 
question private: false
success Saved package.json
✨  Done in 35.73s.
```

vue.jsを入れます。

```shell-session
$ yarn add vue
```

適当に２つコンポーネントを作ります。

```shell-session
$ mkdir -p src/components
```

```html:src/components/Foo.vue
<template>
  <div class="foo">
    Hello Foo {{msg}}
  </div>
</template>

<script>
export default {
  props: {
    msg: {
      type: String,
      required: true,
    }
  }
}
</script>

<style>
.foo {
  color: blue;
}
</style>
```

```html:src/components/Bar.vue
<template>
  <div class="bar">
    Hello Bar {{msg}}
  </div>
</template>

<script>
export default {
  props: {
    msg: {
      type: String,
      required: true,
    }
  }
}
</script>

<style>
.bar {
  color: red;
}
</style>
```

# webpack設定

webpackを設定していきます。
必要なものをまるっと追加してしましましょう。

```diff:package.json
  {
    "name": "my-components-sample",
    "version": "1.0.0",
    "description": "My Component Sample",
    "main": "index.js",
    "license": "MIT",
    "private": false,
    "dependencies": {
      "vue": "^2.5.9"
-   }
+   },
+   "devDependencies": {
+     "autoprefixer": "^7.1.4",
+     "babel-core": "^6.26.0",
+     "babel-loader": "^7.1.2",
+     "babel-preset-env": "^1.6.0",
+     "clean-webpack-plugin": "^0.1.17",
+     "cross-env": "^5.1.1",
+     "css-loader": "^0.28.7",
+     "style-loader": "^0.18.2",
+     "vue-loader": "^12.1.0",
+     "vue-template-compiler": "^2.3.3",
+     "webpack": "^3.6.0",
+     "webpack-merge": "^4.1.0",
+     "webpack-node-externals": "^1.6.0"
+   }
  }
```

```shell-session
$ yarn install
```

```js:webpack.config.js
const path = require('path');
const webpack = require('webpack');
const merge = require('webpack-merge')
const CleanWebpackPlugin = require('clean-webpack-plugin');

const baseConfig = {
  output: {
    path: path.resolve(`${__dirname}/dist/`)
  },
  module: {
    rules: [
      {
        test: /\.vue$/,
        use: {
          loader: 'vue-loader',
        }
      }
    ]
  },
  resolve: {
    extensions: ['.vue', '.js'],
    alias: {
      '@src': path.resolve(__dirname, 'src'),
      '@components': path.resolve(__dirname, 'src', 'components'),
      'vue$': 'vue/dist/vue.esm.js'
    }
  },
  externals: {
    'vue': 'Vue'
  },
  plugins: [
    new webpack.ProvidePlugin({
      'Vue': 'vue'
    })
  ]
};

let config;
if (process.env.NODE_ENV === 'production') {
  const productionConfig = merge(baseConfig, {
    plugins: [
      new CleanWebpackPlugin(['dist']),
      new webpack.optimize.UglifyJsPlugin({
        minimize: true,
        sourceMap: true,
        mangle: true,
        compress: {
          warnings: false
        }
      }),
      new webpack.LoaderOptionsPlugin({
        minimize: true
      })
    ],
    devtool: '#source-map'
  });
  config = productionConfig // TODO
} else {
  console.error(`\`${process.env.NODE_ENV}\` is not defined.`);
}

module.exports = config;
```

この状態ではまだ`output`もちゃんと指定していないし、エンドポイントもないのでコンパイルできません。
次に進みましょう。

# 公開する
## CDNで公開する

まずはCDNで公開できるようにします。
CDN用のエンドポイントを作成します。

```src/plugins.js
module.exports = {
  install: function(Vue, options) {
    Vue.component('foo', require('@components/Foo'));
    Vue.component('bar', require('@components/Bar'));
  }
};
```

これをエンドポイントとしたwebpackの設定を書いていきます。

```diff:webpack.config.js
- config = productionConfig // TODO
+ config = [
+   merge(productionConfig,
+   {
+     entry: path.resolve(__dirname + '/src/plugin.js'),
+     output: {
+       filename: 'my-components-sample.min.js',
+       libraryTarget: 'window',
+       library: 'MyComponentsSample'
+     }
+   })
+ ];
```

`libraryTarget: 'window'`, `library: 'MyComponentsSample'`を設定することで、ブラウザで`my-components-sample.min.js`を読み込んだときに`window.MyComponentsSample`で使えるようになります。

package.jsonに下記を追加します。

```diff:package.json
  {
    ...
+   "scripts": {
+     "build": "cross-env NODE_ENV=production webpack --progress --hide-modules"
+   },
    ...
  }
```

すかさずビルド！

```shell-session
$ yarn run build
```

`dist/my-components-sample.min.js`と`dist/my-components-sample.min.js.map`ができていればOKです。

これらのファイルをCDNで公開するわけですが、これは[jsDelivr](https://www.jsdelivr.com/)を使うと超がつくほど簡単です。

まずは、`packge.json`に以下を追加します。

```diff:packge.json
  {
    ...
    "main": "index.js",
+   "cdn": "dist/my-components-sample.min.js",
    ...
  }
```

`main`は後で使うので書き換えません。

次に、今まで作ったコードをGitHubで公開しましょう。

```shell-session
$ git init
```

```text:.gitignroe
node_modules/
npm-debug.log
yarn-error.log
```

`dist`ディレクトリを`.gitignore`に入れないように注意です。
`dist`は公開しないといけないですからね。

```shell-session
$ git add -A
$ git commit -m "Initial commit"
```

**GitHubのリポジトリを作成する部分は省略します。**

```shell-session
$ git remote add origin git@github.com:you/my-components-sample.git
$ git push -u origin master
```

タグも付けてあげます。

```shell-session
$ git tag v1.0.0
$ git push origin v1.0.0

```

なんと、これだけでCDNでの公開ができてしまっています。
実際にjsbinで確認してみましょう。

http://jsbin.com/yufohim/edit?html,js,output

やりました。ちゃんと動いています。
(vue.js 2.0.3ではなぜかエラーになって動かなかったので最新バージョンのvue.jsを使っています。)

## NPMで公開する

次に、NPMで公開します。
NPM用のエンドポイントを作成します。

```js:src/index.js
import MyFoo from '@components/Foo';
import MyBar from '@components/Bar';

export const VERSION = '1.0.0';

export const Foo = MyFoo;
export const Bar = MyBar;
```

これをエンドポイントとしたwebpackの設定を書いていきます。

```diff:webpack.config.js
  config = [
    merge(productionConfig,
    {
      entry: path.resolve(__dirname + '/src/plugin.js'),
      output: {
        filename: 'my-components-sample.min.js',
        libraryTarget: 'window',
        library: 'MyComponentsSample'
      }
-   })
+   }),
+   merge(productionConfig,
+   {
+     entry: path.resolve(__dirname + '/src/index.js'),
+     output: {
+       filename: 'my-components-sample.js',
+       libraryTarget: 'umd',
+       library: 'MyComponentsSample',
+       umdNamedDefine: true
+     }
+   }),
  ];
```

`yarn run build`を実行して`dist/my-components-sample.js`と`dist/my-components-sample.js.map`ができていればOKです。

あとは`package.json`を書き換えます。

```diff:package.json
- "main": "index.js",
+ "main": "dist/my-components-sample.js",
```

あとは、npm に push するだけです。

**npmのアカウント作成部分は省略します。**

**npmを汚さないために今回は実際にはpushしません。**

```shell-session
$ yarn publish
```

うまく行っていれば、以下のようにして使うことができるはずです。

```shell-session
$ yarn add my-components-sample
```

```html:hoge.vue
<script>
import * as My from 'my-components-sample'

export default = {
  components: {
    'my-foo': My.Foo
  }
};
</script>
```

# 落ち穂拾い
## デプロイを楽にする

デプロイ手順は以下のようになります。

1. `package.json`と`index.js`のバージョン書き換え
2. `yarn run deploy`
3. `git add -A && git commit -m 'hogehoge'`
4. `git tag v1.x.x && git push origin v1.x.x`
5. `yarn publish`

毎回これをやるのは面倒なので半自動化しましょう。

`deploy.sh`を作ります(著者はshellが苦手なのでソースが汚いのは勘弁)。

```bash:deploy.sh
GEEEN="\e[0;32m"
RED="\e[0;31m"
RESET="\e[0m"
VERSION=$1

# Check version format
if [[ $VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] ; then
  printf "${GEEEN}Deploy version v$VERSION\n${RESET}"
else
  printf "${RED}VERSION format is 'x.x.x'. ($VERSION)\n${RESET}"
  exit 1
fi

# Check existing tag
git tag | grep v$VERSION > /dev/null
if [ $? -eq 0 ]; then
  printf "${RED}git tag 'v$VERSION' is existing\n${RESET}"
  exit 1
fi

printf "${GEEEN}Rewite src/index.js\n${RESET}"
gsed -r -i "s/VERSION = '[0-9]+\.[0-9]+\.[0-9]+'/VERSION = '$VERSION'/g" src/index.js
git add src/index.js

printf "${GEEEN}Rewite package.json\n${RESET}"
gsed -r -i "s/\"version\": \"[0-9]+\.[0-9]+\.[0-9]+\"/\"version\": \"$VERSION\"/g" package.json
git add package.json

printf "${GEEEN}Build\n${RESET}"
yarn run build
git add dist

printf "${GEEEN}git commit\n${RESET}"
git commit -m "Upgrade to v$VERSION"
git tag v$VERSION

printf "${GEEEN}Please command 'git push origin master && git push origin v$VERSION && yarn publish'\n${RESET}"
```

実行権限をつけるのを忘れずに。

```shell-session
$ chmod 774 deploy.sh
```

これを実行するためにはmacの場合`gnu-sed`が必要です。

```shell-session
$ brew install coreutils
$ brew install gnu-sed
```

Linuxの場合は`gsed`を`sed`に置き換えればいいと思います(たぶん)。

`package.json`にdelpoyコマンドを追加してあげましょう。

```diff:package.json
  "scripts": {
    "build": "cross-env NODE_ENV=production webpack --progress --hide-modules",
+   "deploy": "./deploy.sh"
  },
```

あとは、`yarn run deploy 1.0.0`などと実行すれば、最後のpushの直前までやってくれます。

## ドキュメントを書く

せっかく、ライブラリを公開したのですから使ってもらうようにドキュメントを書くといいでしょう。
vue.jsのコンポーネントのドキュメントを書くのには[propdoc](https://github.com/propellant/doctor)を使うのが良さそうです。

propdoc用にコンポーネントを加筆しましょう。

```diff:Foo.vue
  <script>
  export default {
+   name: "Foo",
+   introduction: "displaoy message.",
+   description: `
+     This is amazing component.
+   `,
+   token: "<foo msg='ALL OK'></foo>",
    props: {
      msg: {
        type: String,
        required: true,
+       note: 'message'
      }
    }
  };
  </script>
```

これは以下のように使います。

```html:ComponentDoc.vue
<template>
  <div class="component-doc">
    <article v-for="doc in docs">
      <h1>{{doc.name}}</h1>
      <h2>{{doc.introduction}}</h2>
    </article>
  </div>
</template>

<script>
  import propDoc from 'propdoc'
  import * as My from 'my-components-sample'

export default {
  data: ->
    docs: [
      propDoc.getDoc(My.Foo),
      propDoc.getDoc(My.Bar),
    ]
}
</script>
```

もっと詳しく知りたい人は[公式](https://github.com/propellant/doctor)を確認して見てください。

# まとめ

コンポーネントライブラリを作るのは結構かんたんなのでみんさんもオレオレライブラリを作って公開しましょう。
著者も[Rhoa](https://github.com/isuke/rhoa)というライブラリを作っています。楽しいです。

# 参考

- [How To Publish Your Vue.js Component On NPM](https://www.codementor.io/vuejsdevelopers/how-to-publish-your-vue-js-component-on-npm-aiamf6qzi)
- [Publishing Components in Vue.js](https://vuetiful.wordpress.com/2017/05/13/publishing-components-in-vue-js/)