この記事は mediba advent calendar 2017 23日目の記事です。
フロントエンド開発部の武田 @tkdn が担当します。
プロダクション・趣味の素振りに限らず、自分の鋳型をよっこらせと持ってきて作り始めたり、要件に合わせて設定を足し引きしたりしているのですが、package.json の devDeps も webpack.config.js も見たくなくなります。混雑してくると npm scripts に呪文感が出てきます。
今日は、その辺の構築ストレスを軽減するために、自分のためにポータブルなガチ袋と詰め込むツーリングを作ったという話です。
お送りしたい人
時代は Zero Configuration らしいんですが、今日は webpack の話をします。ターゲットはこんな方向けです。
- webpack をよく使う・まだまだ使っていきな人
- Zero Config がプロダクション向きではないと考えている人
- 素振りする時にいつもの鋳型こねこねするのが面倒な人
ガチ袋?
あまり馴染みがないと思いますが、舞台設営やスタッフの方、大工さん、内装屋さんなどが道具を入れるため腰に巻くベルトです。
シンプルなオンリーワンのツールだと汎用性に欠けるので、各種ツーリングを入れるためのガチ袋をイメージして、必要なものだけを積んで現場に持ち込みたいというのが今回のモチベーションです。
どう作るか
やりたいことの構想としては下記のようになります。
- 作るパッケージすべてが相互依存するので yarn workspace と lerna でフラットに依存をモノレポ管理してレジストラにパブリッシュ
- ガチ袋 = webpack をベースにした cli
- ガチ袋に詰め込むツーリングを設定から読み込める
- Babel = babel-loader で JavaScript をトランスパイルする webpack.config
- 2 の設定からオプションを受け取れるインタフェースでエクスポート
- Preset = 3 で使用するための babel-preset
- 環境に合わせて柔軟にトランスパイルしたい = @babel/preset-env ベース
- Sass = sass-loader で scss ファイルを書き出す webpack.config
- 2 の設定からオプションを受け取れるインタフェースでエクスポート
- 環境に合わせて柔軟に autoprefixer したい
参考にすべき先生たち
参考にするにはすべておそれ多いんですが
- https://github.com/egoist/poi
- https://github.com/jaredpalmer/backpack/
- https://github.com/facebookincubator/create-react-app/
この辺が参考になりそうです。
で、作った
- モノレポ: tkdn/toolbelt
- @tkdn/toolbelt-cli - npm
- @tkdn/toolbelt-babel - npm
- @tkdn/babel-preset-toolbelt - npm
- @tkdn/toolbelt-sass - npm
ここから長くなりそうなので、とりあえず動かしたい人はサンプル見てください。
1. yarn workspace, lerna 準備
yarn workspace は v1 からの機能で、ワークスペースにより複数のパッケージを設定する際に、 yarn install を一度実行するだけで、それらの全てが単一のパスにインストールされるようになります
。下層のパッケージ指定を
package.json に。
{
"workspaces": [
"packages/*"
]
}
なお、リポジトリルートには下記記述の .yarnrc
が必要です。
workspaces-experimental true
lerna は適宜入れていただき、ドキュメントに沿って init するなりしてください。lerna で自分がちょっとハマったのは namescoped なパッケージのパブリッシュ時に private パッケージと判別され lerna run publish
がコケるので、下記のような記述が下層の package.json に必要な点です。
{
"publishConfig": {
"access": "public"
}
}
2. CLI
コマンドとして dev
, build
だけ受け取れるようにしました。設定ファイルはオプションで受け取れます。
Usage: toolbelt-cli
コマンド:
toolbelt-cli dev For develoment, toolbelt watches and builds files.
toolbelt-cli build For production, toolbelt builds files.
オプション:
-v, --version バージョンを表示 [真偽]
-h, --help ヘルプを表示 [真偽]
-c, --config toolbelt setting file(JSON) [デフォルト: "./.toolbeltrc"]
- dev =>
process.env.NODE_ENV === develoment
で監視しつつビルド - build =>
process.env.NODE_ENV === production
でビルド
設定ファイル
{
"dev": {
"targets": {
"browsers": ["last 1 Chrome versions"]
},
"browserSync": "./config/bsconfig.js",
"webpack": [
"./config/toolbelt.babel.js",
"./config/toolbelt.sass.js"
]
},
"build": {
"targets": {
"browsers": ["Android >= 4", "safari >= 9"]
},
"webpack": [
"./config/toolbelt.babel.js",
"./config/toolbelt.sass.js"
]
}
}
トップレベルのフィールドは各コマンドです。
targets
フィールド
browserlist の DSL を記述し、babel, autoprefixer に利用します
browserSync
フィールド
**このフィールドはなくても動きます。**ただ dev-server 的なものは起動しないのでソースファイルを監視しながらファイルがディスクに書き出されるだけです。
個人的には、バックエンドのアプリにプロキシして特定のアセットディレクトリのみプロキシサーバに配信したいシーンがどうしても多いので設定に組み込みます。webpack-dev-server だと route でしかマッピングできなかった認識なのですが間違ってたら教えて下さい。
browserSync の middleware として webpack-dev-middleware を差し込む構成です。この場合だと、ファイルはオンメモリ上にあるのでディスクに書き出しません。
あとはレスポンスだけほしい 未開発の API をモックしたい時に middlware として差し込んだりもするので browserSync にしてます。
webpack
フィールド
配列にある設定ファイルの数だけ webpack([configA],[configB]...)
するイメージです。
config 自体は module.exports = {}
な設定オブジェクトそのままエクスポートしたものでも大丈夫ですが、下記のように設定ファイルからオプションオブジェクトを引数にして CLI 側の webpack に渡すことが可能です。
module.exports = options => {
const {
targets, // 設定ファイルから読まれるフィールドが入ってきます
hasBabelrc, // 作業ディレクトリルートに `.babelrc` があれば true ※ 3 で使用
env // process.env.NODE_ENV が格納されて返ってきます
} = options
// options に入ったプロパティ値を元に可変させる
plugins: env === 'production'
? [new webpack.optimize.UglifyJsPlugin()]
: []
)
}
コード
CLI は雑に書くと下記のようなことをやってるけだけです。
const compiler = webpack(mergedConfig)
if (command === 'dev') {
compiler.watch(true, (err, stats) => {})
}
if (command === 'build') {
compiler.run((err, stats) => {})
}
3. toolbelt-babel
上記でも受け取れる options(arg1) と webpack 設定(arg2) を引数にして、このパッケージ自身が提供する webpack の設定と arg2 をマージして設定を返却します。もうこの辺からお前は何を言っているんだ感ありますね。
module.exports = (options, config) => {
merge({
// パッケージの webpack 設定
}, config)
}
内部的には options.hasBabelrc === true で 作業ディレクトリ直下の .babelrc を優先し、false なら 4. に options.targets を渡して preset とします。ますます説明が混乱してます。下手くそか。
options.env で sourcemap/UglifyJS なども切り替えます。
4. babel-preset-toolbelt
だんだん面倒になってきたのでパッケージのソースそのまま貼ります。babel-preset-env に設定ファイルのtargets
フィールドのオブジェクトそのまま渡すだけのパッケージです。
module.exports = (ctx, options) => {
const { debug } = options
return {
presets: [
[require.resolve('babel-preset-env'),
{
debug: debug || false,
targets: options.targets || { browsers: ['last 1 Chrome versions'] },
modules: false,
loose: true,
useBuiltIns: true
}
]
],
plugins: [
[require('babel-plugin-transform-class-properties'), { loose: true }],
[require('babel-plugin-transform-object-rest-spread'), { useBuiltIns: true }]
]
}
}
です。
※ 記事とは関係ないですが、この構成のまま Babel7 に総じて上げてみましたが、ソース上のパッケージ名と useBuiltins: 'usage'
以外は変更するところがありませんでした。
5. toolbelt-sass
3.のインタフェースと同じで、targets
フィールドのオブジェクトを受け取って autoprefixer(targets) 噛ましたり、options.env で sourcemap 吐き出す/outputStyle 変える、option.includePaths があれば指定モジュールまでのパスを通す、くらいです。
サンプル再掲
実行するとこんな感じです。
求めていたガチ袋と俺の CLI、袋に詰めるツーリングが数個出来上がりました。
まとめ
まあまあ満足できるガチ袋ができたのですが、これは私が求めているもので、皆さんが求めているものとは違うかもしれません。
人には人のガチ袋。
もしガチ袋が必要なら自分で作ってみるのもいいんじゃないでしょうか。