examples-web-app 更新 2016/5

2016年5月25日 0 開発 , , , ,

akabekobeko/examples-web-app にある front-end-starter と front-end-starter-with-gulp を更新した。ここ最近の開発で得られた知見や方針を反映している。それらについては Twitter でもつぶやいていたのだけど、せっかくなので記事にまとめておく。

静的リソース用フォルダの変更

従来のフォルダ構成は

.
├── package.json
├── src/
│   ├── index.html
│   ├── fonts/
│   ├── js/
│   └── stylus/
└── test/

となっていた。src/ 直下と Web サイトのルートを一致させていたのだが JavaScript と Stylus の
開発用リソースと index.htmlfonts のような静的リソースを区別しにくかった。そこで構成を以下のように変更。

.
├── package.json
├── src/
│   ├── assets/
│   │   ├── fonts/
│   │   └── index.html
│   ├── js/
│   └── stylus/
└── test/

静的リソースは assets に置かれる。JavaScript や Stylus のコンパイル結果は assets に出力される。今後、例えば画像を静的リソースとして追加する場合は src 直下ではなく assets 配下に置く。静的なものと開発用フォルダを分けたことで見通しがよくなった。

browser-sync

Stylus の Source Maps 参照は元ファイルの相対パス指定が必要となる。そのため browser-sync の表示対象としたフォルダ内にそれらが含まれていなければならない。しかし assets をルートすると stylus フォルダが見えないので Source Maps を参照できなくなる。

この問題を解決するためにはルートを src に変更した場合、npm としてインストール & 参照している normalize.css が含まれない。よってプロジェクト全体のルートになる ./ を指定する。

この状態で browser-sync を起動すると Web ブラウザで初期表示される階層が ./ になるため、src/assets を表示するためには手動で URL を修正しなければならない。これは面倒なので、初期表示するページも同時に指定しておく。

{
  "scripts": {
    "watch:server": "browser-sync start --server ./ --startPath src/assets/"
  }
}

--server に指定されたパスが Web サイトのルートになる。初期表示するページはルートからの相対パスとして --startPath へ指定すればよい。index.html はデフォルトの表示対象なので省略可能。別のページにするなら明示的に指定してもよい。

これでプロジェクト配下にある全てのファイルとフォルダを参照できるようになった。

Stylus 関連

これまで Stylus の CLI 設定は npm-scripts で以下のようにし定義ていた。

{
  "scripts": {
    "build:css": "stylus -c ./src/stylus/App.styl -o ./src/html/bundle.css -m --sourcemap-root ../stylus",
    "watch:css": "stylus -c -w ./src/stylus/App.styl -o ./src/html/bundle.css -m --sourcemap-root ../stylus",
    "release:css": "stylus -c ./src/stylus/App.styl -o ./dist/bundle.css"
  }
}

Source Maps における元ソースの参照を --sourcemap-root で指定していたのだが、いつからかこの方法だと Not Found になっていた。改めて Executable — Stylus を見直したら Source Maps 関連のオプションに --sourcemap-base というものがある。相対パスで指定する場合、これを利用するのが正しいので修正した。

それと Normalize.css を npm で管理して Stylus に組み込むに書いた内容を反映した npm-scripts は以下となる。

{
  "scripts": {
    "build:css": "stylus -c --include-css ./src/stylus/App.styl -o ./src/html/bundle.css -m --sourcemap-base ../stylus",
    "watch:css": "stylus -c -w --include-css ./src/stylus/App.styl -o ./src/html/bundle.css -m --sourcemap-base ../stylus",
    "release:css": "stylus -c --include-css ./src/stylus/App.styl -o ./dist/bundle.css"
  }
}

Source Maps の相対パスが ./stylus ではなく ../stylus になっているのは、前述の静的リソース用フォルダ変更への対応となる。

JavaScript 関連

Babel の設定を .babelrc から package.json の babel プロパティに移動した。mocha についても espower-babel から babel-preset-power-assert への移行で書いたようにしばらく mocha.opts で運用していたのだが、Babel にあわせて package.json の npm-scripts へ定義する方法に戻した。

{
  "babel": {
    "presets": [
      "es2015"
    ],
    "env": {
      "development": {
        "presets": [
          "power-assert"
        ]
      }
    }
  },  
  "browserify" : {
    "transform": [
      "babelify"
    ]
  },
  "scripts": {
    "test": "mocha --compilers js:babel-register test/**/*.test.js",
    "build:js": "browserify ./src/js/App.js -d | exorcist ./src/assets/bundle.js.map > ./src/assets/bundle.js",
    "watch:js": "watchify -v -t [ babelify ] ./src/js/App.js -o \"exorcist ./src/assets/bundle.js.map > ./src/assets/bundle.js\" -d",
    "release:js": "cross-env NODE_ENV=production browserify ./src/js/App.js | uglifyjs > ./dist/bundle.js"
  }
}

Browserify の transform 設定も package.json の browserify.transform に定義して CLI オプションから -t [ babelify ] を削除している。しかし watchify はこれを無視するらしく、watch が落ちてしまう。コンパイルにも失敗しているようで bundle.js の実処理は空だ。仕方ないので watchify だけオプションを残している。

もうひとつ、production ビルドについて。

front-end-starter では View や Flux 系の npm は組み込まない方針だが、React などを追加した場合、そのまま require/import するとデバッグ用の処理が大量に残る。それらは

if (process.env.NODE_ENV !== 'production') {
}

のようになっているため、残ったとしてもさほど実害はない。しかしサイズは巨大だし React のリリース用イメージである react.min.js からは除去されているものだから自前で bundle する場合もそのようにしたい。

これを実現するためには Babel のビルド時に NODE_ENV=production を指定すればよい。npm-scripts で実行するなら環境変数の設定だけでなく、その記法もクロスプラットフォーム対応させたいので cross-env を利用する。前述のサンプルから当該部分だけ抜き出すと

{
  "babel": {
    // Babel 設定
  },  
  "browserify" : {
    // browserify 設定
  },
  "scripts": {
    "release:js": "cross-env NODE_ENV=production browserify ./src/js/App.js | uglifyjs > ./dist/bundle.js"
  }
}

のようになる。

この話と除去の原理については browserify + npmでReactを使う場合はNODE_ENVを設定するとよい – Qiita を参照のこと。Downloads | React の Note でも production と mishoo/UglifyJS2 について言及されている。

React v15.1.0 を bundle する場合、この処理の有無でファイルサイズが 8KB ぐらい縮小された。

gulpfile を ES2015 対応させる

いまは gulp を利用していないのだけど、何気なく front-end-starter-with-gulp の npm を更新してみたら gulp-watchify が更新されていて最新 Browserify に対応しているようだったので、これも最新構成に修正してみた。

gulp v4 を目前に控えており、そちらでは gulp-load-plugins で実現していた処理が標準化されるなどタスク実装を改善するレベルの機能追加がある。それを待つつもりだったが、そう考えてから数ヶ月すぎて未だ v3.9 のままなので、現時点で可能な ES2015 対応だけ反映することにした。

しかし 2016/5/25 時点の gulpjs/gulp を babel や ES2015 で検索しても gulp/exports-as-tasks.md とか README、CHANGELOG ぐらいしか情報がない。機能としては実装されているが公式リファレンスはこれからなのだろうか。

gulpfile ES2015 とかでググると gulpfileをES2015(ES6)で書くUsing ES6 with gulp などが見つかった。後者の記事では Babel 6 以降の変更を反映しているため、主にこちらを参考にする。

ES2015 対応を試すにあたり、問題が起きたときの切り分けを簡単にするため最小のプロジェクトを用意することにした。npm init して package.json だけ存在する状態から開始する。

{
  "name": "gulp-es2015",
  "version": "1.0.0",
  "description": "gulp-es2015",
  "author": "akabeko",
  "license": "MIT",
  "main": "index.js",
  "scripts": {
    "test": ""
  }
}

はじめに必要な npm を揃える。参考記事では gulpbabel-corebabel-registerbabel-preset-es2015 を利用しているので、まとめてインストール。

$ npm i -D gulp babel-core babel-register babel-preset-es2015

なお babel-core は babel-register をインストールすると依存で入る。そのためか明示的にインストールしなくても ES2015 版の gulpfile を処理できるのだが、公式リファレンスの言及がないので参考記事に従い、すべて入れておいた。

Babel の preset 設定は package.json に定義。依存や設定は可能な限り package.json で管理する方針。

gulp の default タスクを npm-scripts から呼び出すように定義。こうすると npm start でプロジェクトのローカルにある gulp を使用するのでグローバルにインストールしなくても済む。

これらをすべて反映した状態の package.json。

{
  "name": "gulp-es2015",
  "version": "1.0.0",
  "description": "gulp-es2015",
  "author": "akabeko",
  "license": "MIT",
  "main": "index.js",
  "babel": {
    "presets": [
      "es2015"
    ]
  },
  "scripts": {
    "start": "gulp"
  },
  "devDependencies": {
    "babel-core": "^6.9.0",
    "babel-preset-es2015": "^6.9.0",
    "babel-register": "^6.9.0",
    "gulp": "^3.9.1"
  }
}

環境が整ったので gulpfile を実装する。ES2015 で書く場合はファイル名を gulpfile.babel.js にしておく。すると gulp が babel-register 経由で babel-preset-es2015 を呼び出して ES2015 で記述されたファイルをコンパイル & 実行という流れで処理される仕組みのようだ。

gulpfile.babel.js を実装。console.log するだけの簡素なタスクを定義しておく。

import gulp from 'gulp';

gulp.task( 'default', () => {
  console.log( 'test' );
} );

実行してみる。

$ npm start

> gulp-es2015@1.0.0 start .../sample
> gulp

[16:20:03] Requiring external module babel-register
(node:8531) fs: re-evaluating native module sources is not supported. If you are using the graceful-fs module, please update it to a more recent version.
[16:20:04] Using gulpfile .../sample/gulpfile.babel.js
[16:20:04] Starting 'default'...
test
[16:20:04] Finished 'default' after 208 μs

タスクは正常に実行された。

しかし gulp の参照している graceful-fs が古いためだろうか、常に警告が表示される。なんとかして欲しいものだ。

gulp-stylus の Source Maps 修正

gulp 版の browser-sync も npm-scripts と同様に server と startPath を分けて見たのだが、Stylus の Source Maps がうまく参照できなかった。調査したところ、gulp.src で base オプションを指定すれば適切に参照できることがわかった。

また front-end-starter と同じく normalize.css を npm で管理して組み込むように修正してみた。それら全てを反映した css タスクは以下となる。

gulp.task( 'css', () => {
  return gulp.src( [ config.dir.stylus + 'App.styl' ], { base: config.dir.root } )
    .pipe( $.plumber() )
    .pipe( $.if( !( config.isRelease ), $.sourcemaps.init() ) )
    .pipe( $.stylus( { 'include css': true } ) )
    .pipe( $.rename( 'bundle.css' ) )
    .pipe( $.if( config.isRelease, $.cleanCss() ) )
    .pipe( $.if( !( config.isRelease ), $.sourcemaps.write( './' ) ) )
    .pipe( $.if( config.isRelease, gulp.dest( config.dir.dist ), gulp.dest( config.dir.assets ) ) );
} );

import や config の定義については examples-web-app/gulpfile.babel.js を参照のこと。

修正を反映した後に npm start して browser-sync が適切にページを表示すること、その状態から Source Maps を参照できることを確認済み。

npm-scripts でクロスプラットフォームに環境変数を参照するための npm を作成してみた

2016年5月19日 0 開発 , , , ,

以下の記事で npm-scripts から環境変数を参照する方法と問題点について書いた。

課題として npm run するプラットフォームによって変数の参照記法が異なるため、それらを統一できないという問題がある。npm-scripts でタスク管理したい派としては、これをどうしても解決したかったので、そのための npm を作成してみた。

以下に npm の設計などをまとめる。

npm-scripts における環境変数の参照記法

冒頭のリンク先でも解説してあるが、改めて。package.json に定義された値を npm-scripts から環境変数として参照する場合の記法は以下となる。

Platform Format
OS X, Linux $npm_package_NAME or $npm_package_config_NAME
Windows %npm_package_NAME% or %npm_package_config_NAME%

今のところ標準の npm run でこれを統一する方法はないらしい。よってプラットフォーム毎に script を分けるか cross-env のような npm により scripts を wrap して参照を解決しなくてはならない。

cross-conf-env

というわけで、npm-scripts 内の環境変数に対する参照記法をクロスプラットフォームに解決する npm を開発してみた。名づけて cross-conf-env。cross-env のアイディアと設計を参考にしたので、それに準じた命名にしてある。

まずはインストール。package.json が既に定義されている状態としてコマンドを実行。

$ npm i -D cross-conf-env

cross-conf-env では以下の参照記法をサポートしている。

Platform Format
OS X, Linux $npm_package_NAME or $npm_package_config_NAME
Windows %npm_package_NAME% or %npm_package_config_NAME%
独自 npm_package_NAME or npm_package_config_NAME

これらのどれを採用してもよいし、混在も許可している。独自を選ぶと記法が統一できる。それ以外を選んだ場合はプラットフォーム、cross-conf-env の順に記法が解決される。

package.json の利用例。

{
  "name": "sample",
  "version": "1.0.0",
  "config": {
    "app": "MyApp"
  },
  "scripts": {
    "var": "cross-conf-env echo npm_package_config_app npm_package_version",
    "var:bash": "cross-conf-env echo $npm_package_config_app $npm_package_version",
    "var:win": "cross-conf-env echo %npm_package_config_app% %npm_package_version%"
  },
  "devDependencies": {
    "cross-conf-env": "^1.0.0"
  }
}

script の先頭に cross-conf-env を宣言、その後に実行したいコマンドを続ける。cross-conf-env はそれらを引数として扱い、環境変数の参照を検出したら解決してから子プロセスとしてコマンドを実行する。

設計について。

npm-cross-conf-env/cross-conf-env.js のみで簡潔する小さな実装。これを npm の体裁でくるんでいるだけ。おこなっていることも単純で、

  1. process.env から npm_package_ を接頭語とするプロパティを列挙
  2. process.argv の index = 1 以降を抽出、argv とする
  3. argv から 1 のプロパティ名を含む値を検索
  4. もし含むならその部分を process.env の当該プロパティの値に置換
  5. argv の先頭をコマンド、以降を引数として子プロセス起動

という感じ。

より実践的な利用方法

akabekobeko/examples-electron の各プロジェクトにある package.json を参照のこと。

これらは npm-scripts を共通にしつつ、実行ファイル名やパッケージ化に使用する Electron のバージョンを config に切り出している。そのため流用が容易になり、設定を変えたくなった場合も長大な script を慎重に直すのではなく config を書き換えるだけで済む。

環境変数の解決により npm-scripts からハードコード部分を減らせる。これを前提にするとタスクランナーとしての実用性がグッと増すはず。

npm-scripts で自前の環境変数を利用する方法と問題点

2016年5月17日 0 開発 , , ,

Electron を試す 7 – Electron v1.0 対応で npm-scripts に環境変数を定義してそれを scripts 内から参照する方法を書いたのだが、この方法は Windows 環境だと利用できない。

と、これだけで話を終えるのはもったいないので、私がおこなった調査や見解についてまとめる。

2016/5/19 追記
本記事の問題を解決するための npm を作成した。詳しくは npm-scripts でクロスプラットフォームに環境変数を参照するための npm を作成してみたを参照のこと。

package.json の config

package.json に config の説明がある。

A “config” object can be used to set configuration parameters used in package scripts that persist across upgrades. For instance, if a package had the following:

{ “name” : “foo”
, “config” : { “port” : “8080” } }

and then had a “start” command that then referenced the npm_package_config_port environment variable, then the user could override that by doing npm config set foo:port 8001.

config に設定した値は npm_package_config_NAME として参照できる。config にはプログラム内から参照する例として以下のサンプルが掲載されている。

http.createServer(...).listen(process.env.npm_package_config_port)

npm-scripts から config の環境変数を参照する

冒頭の記事にも書いた方法。package.json の config に定義した環境変数を npm-scripts から参照する。

{
  "config": {
    "app_name": "MyApp"
  },
  "scripts": {
    "var": "echo $npm_package_config_app_name"
  }
}

環境変数は $npm_package_config_NAME という書式で参照する。$npm_package_config_ までが固定で、それ以降が config へ定義したプロパティ名となる。

この状態で npm run してみると、環境変数が展開されることを確認できるはず。

$ npm run var

> electron-starter@1.2.1 var .../use-env-var
> echo $npm_package_config_app_name

MyApp

この機能を利用することで npm-scripts 内の CLI へ指定する値を config に分離できる。

例えば Electron をパッケージ化するために electron-packager を利用するとして、その CLI へ指定する Electron のバージョンやアプリ名をハードコードせずに config で抽象化してみよう。

{
  "config": {
    "app": "MyApp",
    "electron": "1.1.0"
  },
  "scripts": {
    "release:pack-osx": "electron-packager ./dist/src $npm_package_config_app --out=dist/bin --cache=dist/cache --platform=darwin --arch=x64 --version=$npm_package_config_electron --overwrite --asar --icon=res/app.icns",
    "release:pack-win": "electron-packager ./dist/src $npm_package_config_app --out=dist/bin --cache=dist/cache --platform=win32 --arch=x64 --version=$npm_package_config_electron --overwrite --asar --icon=res/app.ico",
    "release:pack-linux": "electron-packager ./dist/src $npm_package_config_app --out=dist/bin --cache=dist/cache --platform=linux --arch=x64 --version=$npm_package_config_electron --overwrite --asar"
  }
}

release:pack-osx、win、linux で config appelectron を共有している。app がアプリの実行ファイル名など、electron がパッケージ化に使用する Electron のバージョンになる。

electron の値を CLI 側にハードコードしていたら、Electron のバージョンが更新されるたびに 3 箇所の修正が必要となる。面倒なうえ CLI 引数の書き換えは複雑なためミスを誘発しやすい。このように厄介な作業を避けられるのは実にありがたい。

この npm-scripts を他のプロジェクトに流用したくなった場合、アプリ名を書き換えることになる。その際も configapp を修正するだけで済むだろう。

プロジェクトに依存する変更箇所を外部化することで汎用性が高まった。

gulp のように Node としてタスク定義するプラットフォームの魅力は、手続き型の処理と変数の利用にある。

これらの内、タスク処理については CLI の工夫や npm-run-all のような補助 npm を利用することで十分に運用できる。不便を感じるとしたら、プロジェクト構成が複雑などの問題があるだろうから、それを見直すことになるだろう。

一方、変数についてはどうにもならずハードコードやむなしとしていたが、これも代替できたので npm-scripts の利便性が大きく向上した。

はずだったのだが、しかし…

環境変数の展開におけるプラットフォーム依存

npm run を実行する shell によって環境変数の参照記法は異なる。OS X や Linux などで利用されている bash なら $variable で Windows の cmd.exe や PowerShell だと %variable% になる。挙動を試すため package.json へ configscripts を以下のように定義する。

{
  "config": {
    "app_name": "MyApp"
  },
  "scripts": {
    "var:bash": "echo $npm_package_config_app_name"
    "var:win": "echo %npm_package_config_app_name%"
  }
}

Windows とそれ以外の環境で var:bashvar:win を実行してみよう。すると一方は環境変数が展開され、他方は環境変数の名前がそのまま出力されることを確認できる。

この調査をしている時に見つけた記事 How to Use npm as a Build Tool にも

The other downside to these configs is that they’re not very Windows friendly – Windows uses % for variable substitution, while bash uses $. They work fine if you use them within a Node.js script, but if anyone knows of a way to get them working in Windows via the shell commands, let me know!

とある。なるほど、つまり config の定義は共通化できても npm-scripts 内の参照記法は区別しなければならないのだ。その他、cross-env ならゆけるかも?と試したものの、環境変数の定義までは成功したけれど参照記法は統一できなかった。残念。

私は package.json だけで npm 依存とタスク処理を完結させたい派なので config による共通化は諦めることにした。タスク運用として各プラットフォーム標準の環境に Node だけ入れたら動くことを理想としているので、Windows に bash を入れるなどの案は採用しない。

将来 npm-scripts 自体が環境変数のプラットフォーム差を吸収してくれるか、そのような npm が登場したら改めて採用を検討する予定。

npm を実装する場合、cross-env みたいに自身の引数として対象となるコマンドを受け取り、自前で config を展開してから shell に受け渡せばよいのだろうか。気が向いたら開発してみるかもしれない。