2017年4月版 jsライブラリ環境作成

## はじめに

jsライブラリを作成するにあたりwebpack周りを調べたメモです。  
webpackの記事はググれば山のように見つかるのですが、
webpackは少し前に触って以来で、いつの間にかwebpack2がリリースされ記述方法が若干変わっており、それまで参考にした記事はwebpack1の書き方だったり、入門者にはなかなか戸惑う点がありました。

というわけで、またそういったノイズ記事を増やしてしまうことについてご了承ください。
2017年4月時点の内容ですので、近い将来また変わってくると思います。

## ゴール

今回、やりたいことは以下です。

- jsはBabelでes2015ベースで書く
- jsはminifyさせる
- cssはSassで書いて、jsにバンドルさせる
- unitテストを書く
- lintを入れる

## 環境

OS: macOS v10.12.4
node: v6.10.2
npm: v4.6.1

## サンプル

最終的に出来たサンプルプロジェクトはこちらです。
https://github.com/darquro/webpack_sample  

プロジェクト構造は以下のようになりました。

```
.
├── dist
│   ├── webpack_sample.js
│   ├── webpack_sample.js.map
│   └── webpack_sample.min.js
├── html
│   └── index.html
├── src
│   ├── css
│   │   └── style.scss
│   └── js
│       ├── friend.js
│       ├── main.js
│       └── person.js
├── test
│   ├── friend_test.js
│   └── mocha.opts
├── .babelrc
├── .eslintrc
├── package.json
├── postcss.config.js
└── webpack.config.js
```

動作確認はこちらで確認できます。
https://darquro.github.io/webpack_sample
ボタンをクリックイベント(js)とボタンのスタイル変更(css)だけ確認できるページです。

各npmパッケージのインストール手順の説明は省いていますので、 `package.json` から必要なものを確認してください。

```json
"devDependencies": {
  "autoprefixer": "^6.7.7",
  "babel-core": "^6.24.1",
  "babel-eslint": "^7.2.3",
  "babel-loader": "^7.0.0",
  "babel-preset-es2015": "^6.24.1",
  "babel-preset-power-assert": "^1.0.0",
  "babel-register": "^6.24.1",
  "concurrently": "^3.4.0",
  "css-loader": "^0.28.0",
  "eslint": "^3.19.0",
  "eslint-config-airbnb-base": "^11.1.3",
  "eslint-plugin-import": "^2.2.0",
  "mocha": "^3.3.0",
  "postcss-loader": "^1.3.3",
  "postcss-smart-import": "^0.6.12",
  "power-assert": "^1.4.2",
  "precss": "^1.4.0",
  "style-loader": "^0.16.1",
  "webpack": "^2.4.1",
  "webpack-dev-server": "^2.4.4"
}
```

## webpack

jsやcss、イメージなどをビルドしてまとめてくれるツール。デファクトスタンダードとなっている。
https://webpack.js.org/
<img width="1010" alt="webpack.png" src="https://qiita-image-store.s3.amazonaws.com/0/92539/ef050456-7896-e370-1a53-ead6ba3c6895.png">

`webpack.config.js`に色々設定を記載します。
今回webpack 2.4.1を使用していますので、書き方がwebpack 1とは一部変わってます。 
作成した設定はこのようになりました。

```js
const Webpack = require('webpack');
const path = require('path');

const MINIFY = process.env.NODE_MINIFY === '1';

module.exports = {
  context: path.join(__dirname, '/src/js'),
  entry: {
    webpack_sample: './main.js',
  },
  output: {
    path: path.join(__dirname, '/dist'),
    filename: MINIFY ? '[name].min.js' : '[name].js',
    publicPath: '/assets/',
  },
  resolve: {
    modules: [
      path.resolve(__dirname, 'src'),
      'node_modules',
    ],
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /(node_modules|bower_components)/,
        use: {
          loader: 'babel-loader',
          options: {},
        },
      },
      {
        test: /\.scss$/,
        use: [
          'style-loader',
          {
            loader: 'css-loader',
            options: {
              importLoaders: 1,
            },
          },
          'postcss-loader',
        ],
      },
    ],
  },
  devtool: 'source-map',
  plugins: MINIFY ? [
    new Webpack.optimize.UglifyJsPlugin(),
  ] : [],
  devServer: {
    contentBase: 'html',
    port: 8080,
  },
};
```

順番に設定内容を説明していきたいと思います。
[公式ドキュメントはこちら](https://webpack.js.org/configuration/)

### context

```js
context: path.join(__dirname, '/src/js'),
```

contextを指定すると、ビルドする対象のルートディレクトリを指定できます。
今回では `src/js` が起点となるディレクトリなので、このように設定しました。

### entry

```js
entry: {
  webpack_sample: './main.js',
},
```
entryには起点となるファイルの名前とパスを指定します。
今回のプロジェクトは `webpack_sample` という名前にしましたので、keyに`webpack_sample` を指定し、パスはcontextで指定した場所から相対パスで起点となる `main.js` を指定しています。

### output

```js
output: {
  path: path.join(__dirname, '/dist'),
  filename: MINIFY ? '[name].min.js' : '[name].js',
  publicPath: '/assets/',
},
```

outputはビルドして出力先を指定します。
パスとしては `dist` ディレクトリに出力し、ファイル名は、 `webpack_sample.js` とminifyした `webpack_sample.min.js` を出力するようにしました。

`MINIFY` の変数については、npmのコマンドに、`NODE_MINIFY=1 webpack` を指定させ、一つの設定を流用しつつ、minifyする箇所だけ設定を分岐するようにしています。

publicPathについては、テスト時にローカルサーバーを立てて確認する際、htmlから参照できるパスを `assets/webpack_sample.js` というパスで参照出来るようにしています。

### resolve

```js
resolve: {
  modules: [
    path.resolve(__dirname, 'src'),
    'node_modules',
  ],
},
```

resolveを指定すると、jsでimportする際、相対パスで `../../foo/bar.js` といったような面倒な指定に陥ることを防げます。　
今回では `src` ディレクトリが起点となっていますので、jsからは `src` 配下のディレクトリからのパスで指定することができるようになります。
`node_modules` は消してしまうとnpmライブラリが見れなくなるらしいので必ず指定します。  

### module.rules (js)

ここにloader系を指定します。
webpack1では、rulesではなくloadersという書き方ですので、webpack1の記事を見てるとハマります。


```js
{
  test: /\.js$/,
  exclude: /(node_modules|bower_components)/,
  use: {
    loader: 'babel-loader',
    options: {},
  },
},
```

jsは今回Babelだけですので、babel-loaderを指定します。
書き方はbabel-loaderのドキュメントそのままです。
https://github.com/babel/babel-loader

optionに何も書いていないのは `.babelrc` に書いているからです。

### module.rules (scss)

```js
{
  test: /\.scss$/,
  use: [
    'style-loader',
    {
      loader: 'css-loader',
      options: {
        importLoaders: 1,
      },
    },
    'postcss-loader',
  ],
},
```

css(scss)側は、 `style-loader` 、 `css-loader` 、 `postcss-loader` の3つを使用しています。
書き方については、postcss-loaderのドキュメントのままです。
https://github.com/postcss/postcss-loader
webpackでSassのコンパイルを検索すると `sass-loader` を使用する記事が幾つか見つかりますが、
ベンダープレフィックスをつける `autoprefixer` が `postcss-loader` を使用する必要があり、[移行したという記事](http://qiita.com/kenichi_odo/items/308328e78737756795e2)がありましたので、 `postcss-loader` を使用しました。

### devtool

```js
devtool: 'source-map',
```

source-mapファイルを出力する設定です。
`source-map` 以外にも色々と指定があります。
https://webpack.js.org/configuration/devtool/#devtool

### plugins

```js
plugins: MINIFY ? [
  new Webpack.optimize.UglifyJsPlugin(),
] : [],
```

minifyする設定です。 `MINIFY` のフラグでそのままビルドしたjsとminifyしたjsの両方を出力するようにしています。
minifyするだけならこの指定をしなくても `$ webpack -p` の指定だけでminifyしてくれます。
将来的にminifyのオプションが何か増えるかもしれないという気もしたので、この指定にしました。

### devServer

```js
devServer: {
  contentBase: 'html',
  port: 8080,
},
```

`webpack-dev-server` コマンドでローカルサーバーを立ち上げるときの設定です。
ルートでhtmlディレクトリを指定しました。

## Babel

Babelの設定は `webpack.config.js` にまとめることもできますが、 `.babelrc` ファイルに記載するようにしました。

```json
{
  "presets": ["es2015", "babel-preset-power-assert"]
}
```

presetsで `es2015` と `babel-preset-power-assert` を指定しています。
`babel-preset-power-assert` についてはunitテストで後述します。

## PostCSS

PostCSSの設定もBabelと同じく `webpack.config.js` にまとめることもできますが、 `postcss.config.js` に記載しました。  

```js
/* eslint global-require: off */
module.exports = {
  plugins: [
    require('postcss-smart-import')({ /* ...options */ }),
    require('precss')({ /* ...options */ }),
    require('autoprefixer')({ /* ...options */ }),
  ],
};
```

Sassの記法を書けるようにするために、3つのプラグインを使用しています。
それぞれの説明については割愛します。
- `postcss-smart-import`: PostCSSで@import記法を使うためのプラグイン
- `precss`: PostCSSで変数定義などを使うためのプラグイン
- `autoprefixer`: PostCSSでベンダープレフィックスを使うプラグイン


## sampleコード

さて、jsライブラリとしての基本的な設定周りが済んだので、今回のサンプルプロジェクトのコードについて説明します。
まずはPersonクラスとそれを継承したFriendクラスを用意しました。

```js
class Person {
  constructor() {
    this.name = 'Person';
  }
  getName() {
    return `This is ${this.name}`;
  }
}
export default Person;
```

```js
import Person from 'js/person';

class Friend extends Person {
  constructor(name) {
    super();
    this.name = name;
  }
  callName() {
    alert(`This is ${this.name}`);
  }
  getName() {
    return `This is ${this.name}`;
  }
}

export default Friend;
```

起点となるmain.jsは以下です。

```js
import 'css/style.scss';
import Friend from 'js/friend';

window.Friend = Friend;
```

また、Sassファイルは以下です。

```scss
$btn-width: 300px;
$btn-hight: 150px;

button {
  &.btn {
    width: $btn-width;
    height: $btn-hight;
  }
}
```

テストのindex.htmlには、以下のようにボタンを配置して、そのクリックイベントとスタイルに今回のライブラリを使用するようになっています。

```html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <script src="assets/webpack_sample.js"></script>
</head>
<body>
  <button class="btn" id="btn">click me</button>
  <script>
  var friend = new Friend('Taro');
  document.getElementById('btn').onclick = function(){
    friend.callName();
  };
  </script>
</body>
</html>
```

ビルドは、 `webpack` コマンドで、サーバー起動は `webpack-dev-server` を使用します。
そのままターミナルから実行するには、webpackのパスが通ってないので、

```sh
$ `npm bin`/webpack
```

とする必要がありますが、package.jsonのscriptにコマンドを登録しているので、それぞれ、

```sh
$ npm run build
$ npm run start
```

で出来るようにしました。

```js
"scripts": {
  "build": "webpack && NODE_MINIFY=1 webpack",
  "start": "webpack-dev-server",
},
```

`webpack-dev-server` はファイルの更新も検知してくれます。

## unitテスト

unitテストはテストフレームワークに[mocha](https://mochajs.org/)、アサーションライブラリに[power-assert](https://github.com/power-assert-js/power-assert)を使用しました。

testディレクトリ配下がテストコードです。
まず `mocha.opts` を作成し、設定を記載します。

```
--recursive
--compilers js:babel-register
```

`recursive`　の指定をすると再帰的にテストファイルを検索してくれるようです。
`compilers js:babel-register` はmochaでes2015で書くためのパッケージです。
また、power-assert側もes2015を解釈させるために、 `babel-preset-power-assert` が必要で、presetsを `.babelrc` で記載していました。

```js
import assert from 'assert';
import Friend from 'js/friend';

describe('test Friend', () => {
  it('getName()', () => {
    const friend = new Friend('Taro');
    assert.equal('This is Taro', friend.getName());
  });
});
```

テストコードはFriendクラスのテストを一つ書きました。

package.jsonにtestコマンドを追加し、mocha側からのパスを解決させています。

```js
"test": "NODE_PATH=$NODE_PATH:$PWD/src mocha"
```


## lint

lintは以下のパッケージを使用しています。

- eslint
- eslint-plugin-import
- eslint-config-airbnb-base

eslint-config-airbnb-baseはAirbnbのスタイルガイドをベースに少しカスタマイズをしました。  
eslintの設定は `.eslintrc` ファイルに記述します。

```js
{
  "extends": "airbnb-base",
  "env": {
    "es6": true,
    "node": true,
    "browser": true
  },
  "rules": {
    "import/no-unresolved": [2, { ignore: ["^js|^css"] }],
    "import/no-extraneous-dependencies": [0],
    "import/extensions": [0],
  },
  "globals": {
    /* MOCHA */
    "describe": false,
    "it": false,
    "before": false,
    "beforeEach": false,
    "after": false,
    "afterEach": false
  }
}
```
`"browser": true` とすることで、ブラウザのもつAPIをエラーにさせないようにしたり、
npmパッケージにないものもimportしてもエラーにならないように、今回のsrcディレクトリ配下は除外するようにしています。

また、mochaのAPIも除外しています。

ここだけでは拾いきれないファイル個別の設定は、ファイルの先頭にコメントを入れるなどで、eslintの固有設定もできます。
postcss.config.jsはglobal-requireをエラーにしないようにしています。

```js
/* eslint global-require: off */
```

### Atom

私はAtomをメインに使ってるので、Atom側のパッケージも入れます。

- Linter
- linter-eslint

をインストールすればeslintが動いてくれます。

## さいごに

ゼロから環境構築まで色々調べながらでとても疲れました。
以上です!