gulp で uglify-es を利用する

2017年6月14日 0 開発 , ,

タスクランナーは npm-scripts 派なのだけど、akabekobeko/examples-web-app に公開している front-end-starter には gulp 版も用意してある。かつて gulp を利用していたこと、世間では現在もそれなりに Web フロントエンド開発で gulp が採用されていることから動向チェック用にメンテナンスしている。

front-end-starter は Electron も含む Web フロントエンド開発環境の変更を気が向いたときに反映しているのだが、そういえば uglify-es を gulp で利用したくなったらどうするのだろう?というのが気になったので試してみた。

gulp-uglify

gulp で uglify-js を利用する場合、gulp wrapper となる gulp-uglify を採用するのが一般的だろう。では uglify-es はどうなのか。uglify-js 本家が uglify-es を独立したパッケージとしているように gulp-uglify-es があるとか?と予想したが gulp-uglify として js/es を切り替えられるようになっていた。

README の Using a Different UglifyJS から引用する。

By default, gulp-uglify uses the version of UglifyJS installed as a dependency. It’s possible to configure the use of a different version using the “composer” entry point.

標準では dependency としてインストールされた uglify-js を使用するが、composer を利用することで js/es を分岐できるとのこと。以下は README に併記されているサンプル コード。

var uglifyjs = require('uglify-js'); // can be a git checkout
                                     // or another module (such as `uglify-es` for ES6 support)
var composer = require('gulp-uglify/composer');
var pump = require('pump');

var minify = composer(uglifyjs, console);

gulp.task('compress', function (cb) {
  // the same options as described above
  var options = {};

  pump([
      gulp.src('lib/*.js'),
      minify(options),
      gulp.dest('dist')
    ],
    cb
  );
});

gulp ストリーム (pipe) をそのまま使用せず pump で wrap している点を除けば、割りと簡単に切り替えられるようだ。

gulp タスク修正

gulp-uglify としては pump を推奨しているようだが、以下のように gulp-watchify と組み合わせていると書き換えが多くなるので、

// ...import や gulp-load-plugins の設定など

gulp.task('js', $.watchify((watchify) => {
  const time = process.hrtime()

  return gulp.src([ config.dir.js + 'App.js' ])
    .pipe($.plumber())
    .pipe(watchify({
      watch: config.isWatchify,
      basedir: './',
      debug: true,
      transform: [ 'babelify' ]
    }))
    .pipe(VinylBuffer())
    .pipe($.if(!(config.isRelease), $.sourcemaps.init({ loadMaps: true })))
    .pipe($.if(config.isRelease, $.uglify()))
    .pipe($.rename('bundle.js'))
    .pipe($.if(!(config.isRelease), $.sourcemaps.write('./')))
    .pipe($.if(config.isRelease, gulp.dest(config.dir.dist), gulp.dest(config.dir.assets)))
    .on('end', () => {
      const taskTime = PrettyHRTime(process.hrtime(time))
      $.util.log('Bundled', $.util.colors.green('bundle.js'), 'in', $.util.colors.magenta(taskTime))
    })
}))

gulp ストリームのまま移行する。

// ...import や gulp-load-plugins の設定など

// 追加
import UglifyES from 'uglify-es'
import UglifyComposer from 'gulp-uglify/composer'

gulp.task('js', $.watchify((watchify) => {
  const time = process.hrtime()

  // uglify-es で minify するための関数設定
  const minify = UglifyComposer(UglifyES, console)

  return gulp.src([ config.dir.js + 'App.js' ])
    .pipe($.plumber())
    .pipe(watchify({
      watch: config.isWatchify,
      basedir: './',
      debug: true,
      transform: [ 'babelify' ]
    }))
    .pipe(VinylBuffer())
    .pipe($.if(!(config.isRelease), $.sourcemaps.init({ loadMaps: true })))
    // uglify-es で minify
    .pipe($.if(config.isRelease, minify({})))
    .pipe($.rename('bundle.js'))
    .pipe($.if(!(config.isRelease), $.sourcemaps.write('./')))
    .pipe($.if(config.isRelease, gulp.dest(config.dir.dist), gulp.dest(config.dir.assets)))
    .on('end', () => {
      const taskTime = PrettyHRTime(process.hrtime(time))
      $.util.log('Bundled', $.util.colors.green('bundle.js'), 'in', $.util.colors.magenta(taskTime))
    })
}))

このサンプルには掲載していないが、別のリリース用イメージ生成タスクで config.isReleasetrue にしてから js タスクを実行することで、uglify-es による minify 処理が走ることを確認できた。

まとめ

Electron や Chrome (Chromium) のように先進的な環境を決め打ちで開発したり、babel-preset-env で環境を抽象化しつつ変換を最小におさえると minify 対象となるコードに ES.next 部分があらわれる。その際は minify ツール側も ES.next 対応が求められるため uglify-esbabili を利用することになるだろう。gulp に関しては本記事のように gulp-uglify の機能として uglify-es を採用するとよい。

ところで gulp-uglify の設計は gulp wrapper における問題へのよき回答にもなっており、感心した。

gulp を想定していない npm を gulp ストリームにのせる場合、vinyl-buffer などで頑張るか、そのような処理で wrap した gulp プラグインとする必要がある。前者はエンド ユーザー負担が大きすぎるので後者を選ぶことが多いのだけど、こちらだと gulp プラグインの dependency で wrap 対象 npm の更新へ追従しなければならない。

gulp-uglify/composer 方式なら自身の dependency が古びても、エンド ユーザー側から任意バージョンの npm を指定できる。つまり gulp 化の恩恵を享受しつつ、最新 npm に置き換えられるのだ。対象 npm のインターフェースが維持されていることは前提となるが、うまい方法である。自身の dependency + 同一インターフェース npm をサポートできる。

私がタスクランナーを gulp から npm-scripts へ移行した理由のひとつは gulp プラグインの陳腐化なのだけど、この設計が普及すれば、この問題に関しては概ね解決しそうである。gulp プラグインのガイドラインに含めてもよいのでは、とも思う。

この記事を書いた後に気付いたのだが babili の gulp 版として gulp-babili が提供されている。こちらは Babel/babili 本家なので、Babel ファミリーを好むなら uglify-es の代りに採用してもよいだろう。

Electron を試す 9 – Babel 変換を最小におさえつつ minify

2017年6月5日 0 開発 , ,

小ネタ。

Electron が採用している Chromium は ECMAScript 対応がかなり進んでいる。よって Babel を使用しつつも変換を最小におさえたくなる。

この点について以前 babel-preset-env と minify という記事を書いたのだが、uglify-js の ES2015 以降への対応が暫定版なため、よりよい選択肢として babel-preset-babili を試してみた。その記事のコメントで mysticatea さんが提案されているように Browserify へ -g オプションをつければ node_modules 部分も含めて minify 可能だが、それでも Browserify の Bundle 処理は minify されない。

よって uglify-js harmony 版が正式リリースされるのを待っていたところ、uglify-es が提供されたので試してみる。

uglify-es

従来 uglify-js で harmony と呼ばれていた ES2015 以降へ対応する仕向け。README によれば uglify-js@3.x 系に対して API と CLI 互換がある。CLI 名も uglifyjs のまま。よって最新の uglify-js を使用しているなら、特に処理を変えず移行可能。

移行は package.jsondevDependencies で uglify-js を uglify-es へ置き換えればよい。uglify-js@3.x から提供となるので、バージョン系は 3.x 以降となる。安全のため npm un -D uglify-js してから npm i -D uglify-es するのがよいだろう。

余談だが Support UglifyJS 3 と関連 issue にて webpack の uglify-es 対応が検討されている。難航しているようだ。

babel-preset-env

uglify-es により ES2015 以降を解析可能となるため、Babel 変換も最小にする。babel-preset-env へ開発で使用している Electron のバージョンを指定。以下は package.json の例。.babelrc なら "babel" プロパティの値をルートにする。

"babel": {
  "presets": [
    [
      "env",
      {
        "targets": {
          "electron": "1.6"
        }
      }
    ]
  ]
}

この設定で akabekobeko/examples-electron のプロジェクトをビルドして classconstlet といった ES2015 以降の機能はそのままに uglify-es で minify されることを確認できた。

問題点

uglify-es は ES2015 なら対応しているけれど async/await を含むコードはエラーになる。試しに async function – JavaScript | MDN のサンプルを含めてみたところ、

Parse error at 0:2932,6
async function add1(x) {
      ^
ERROR: Unexpected token: keyword (function)
    at JS_Parse_Error.get (eval at <anonymous> (.../examples-electron/audio-player/node_modules/uglify-es/tools/node.js:21:1), <anonymous>:86:23)
    at fatal (.../examples-electron/audio-player/node_modules/uglify-es/bin/uglifyjs:286:52)
    at run (.../examples-electron/audio-player/node_modules/uglify-es/bin/uglifyjs:241:9)
    at Socket.<anonymous> (.../examples-electron/audio-player/node_modules/uglify-es/bin/uglifyjs:179:9)
    at emitNone (events.js:110:20)
    at Socket.emit (events.js:207:7)
    at endReadableNT (_stream_readable.js:1045:12)
    at _combinedTickCallback (internal/process/next_tick.js:102:11)
    at process._tickCallback (internal/process/next_tick.js:161:9)

となった。ちなみに Browserify も以前は es6 async class function fails to parse? という問題があったが修正済み。uglify-es の issue [ES8] async/await not supported also in harmony を読むに、近く修正されそうな気がする。

まとめ

async/await 対応の問題はあるものの、babel-preset-env + uglify-es の組み合わせで Electron 向けの Babel 変換を最小におさえられた。

Chrome Canary 60 はフラグ付きで ES Modules が有効になるそうで、これが Electron に採用されたら Bundle と minify 事情も変わりそう。しかし node_modules も含めたサイズ圧縮の観点から Bundle と minify はしばらく必要な処理なので、今後も動向は継続的にチェックしたい。