babelを使ってES2015でJScriptを書こう

Windowsで使えるJScriptはES3ぐらいまでしか使えず、非常につらいです。
~~まあそもそもJscriptを書くこと自体あまりないと思いますが。~~

今回はbabelとwebpackを使ってES2015+でJScriptを書けるようにしてみました。


## 準備

### パッケージインストール
```bash
$ npm i -D json3 babel-core babel-loader babel-polyfill babel-preset-latest \
babel-plugin-transform-es3-member-expression-literals babel-plugin-transform-es3-property-literals \
babel-plugin-transform-jscript \
imports-loader webpack@1.13.2
```

webpackは1.13.2を使用します。
2系はie8のサポートを切るらしいので、jscriptが動かないと思います。
また、1.13.3以降は依存しているUglifyJS 2が2.7系で、圧縮したコードがie8でうまく動きません。
UglifyJS 2のバグが直れば1.14でも大丈夫だと思います。

JScriptではJSONオブジェクトが存在しないため、[json3](https://github.com/bestiejs/json3)をインストールして使えるようにします。
他はwebpackとかbabel関連のパッケージです。

### webpack.config.js

```js:webpack.config.js
module.exports = {
  entry: __dirname + '/script.js',
  output: {
    path: __dirname + '/dist',
    filename: 'bundle.js'
  },
  
  module: {
    loaders: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel',
        query:{
          presets: ['latest']
        }
      },
      {
        test: /\.js$/,
        loader: 'babel',
        query: {
          plugins: [
            'transform-es3-member-expression-literals',
            'transform-es3-property-literals',
            'transform-jscript',
          ]
        }
      }
    ]
  },
  resolve: {
    extensions: ['', '.js']
  }
};
```

自分で書くコードには`babel-preset-latest`を適用します。
読み込まれるすべてのjsには[transform-es3-member-expression-literals](http://babeljs.io/docs/plugins/transform-es3-member-expression-literals/)と[transform-es3-property-literals](http://babeljs.io/docs/plugins/transform-es3-property-literals/)、[transform-jscript](https://babeljs.io/docs/plugins/transform-jscript/)を適用します。

JScriptでは以下のような、予約語のプロパティを使ったコードはエラーになってしまいます。

```js
var hoge = {default: 'hoge'}; // エラー
WScript.Echo(hoge.default); // エラー
```
そのため、[transform-es3-member-expression-literals](http://babeljs.io/docs/plugins/transform-es3-member-expression-literals/)と[transform-es3-property-literals](http://babeljs.io/docs/plugins/transform-es3-property-literals/)を適用し、
以下のようなコードに自動的に変換されるようにします。

```js
var hoge = {'default': 'hoge'}; // OK
WScript.Echo(hoge['default']); // OK
```

[transform-jscript](https://babeljs.io/docs/plugins/transform-jscript/)はJScriptのバグを回避してくれるものらしいです。


### timers.js

JScriptにはsetTimeoutなどがありません。
しかし、`htmlfile`オブジェクトのsetTimeoutが使えるようですので、無理やり使えるようにしてあげます。
webpackでビルドされてもグローバル空間に展開されるように`var`などの修飾子は付けていません。

```js:timers.js
/*global ActiveXObject*/
const htmlfile = new ActiveXObject('htmlfile');
const window = htmlfile.parentWindow;

setTimeout = function wrapSetTimeout(f, t) {
  return window.setTimeout(f, t);
};
clearTimeout = function wrapClearTimeout(id) {
  return window.clearTimeout(id);
};
setInterval = function wrapSetInterval(f, t) {
  return window.setInterval(f, t);
};
clearInterval = function wrapClearInterval(id) {
  return window.clearInterval(id);
};

```


### script.js
```js:script.js
require('./timers.js');
JSON = require('imports?this=>window,this=>global!json3');
require('babel-polyfill');

(() => {
  const text = JSON.stringify([1, 2, 3].map(x => x * 10)).repeat(2);
  WScript.Echo(text); // [10,20,30][10,20,30]
})();
```

先ほどの`timers.js`を読み込んでいます。
また、polyfillを有効にするためにbabel-polyfillを読み込んでいます。
JSONオブジェクトは変数に入れて使います。
requireするときにimports-loaderの機能を使っています。
thisをwindowやglobalに設定して、ビルドインオブジェクトにJSON関連のメソッドを追加できるようにします。
また、グローバル空間に配置することで、他のライブラリからJSONが使えるようにしてあげます。

`babel-polyfill`ではすべてのpolyfillを読み込むため、ファイルサイズが大きくなります。
もし使わないpolyfillは省きたい場合、`core-js`のパッケージを読み込むようにするといいと思います。

```bash
$ npm i -D core-js
```

```js
JSON = require('imports?this=>global!json3');
require('core-js/es5');

require('core-js/es6/object');
require('core-js/es6/function');
require('core-js/es6/array');
require('core-js/es6/string');
require('core-js/es6/promise');
```

## 1つのファイルにまとめる

```bash
$ $(npm bin)/webpack
```

dist/bundle.jsに出力されます。
npm run scriptに書いておくと便利だと思います。

```json:package.json
{
  "scripts": {
    "build": "webpack",
    "watch": "webpack --watch"
  }
}
```

```bash
$ npm run build
```

## JScriptではできなさそうなこと
### getterとsetter
以下のコードのようなgetterやsetterは使えなさそうです。

```js
var foo = {
  get bar() {
    return "bar";
  }
};
```

[transform-es5-property-mutators](http://babeljs.io/docs/plugins/transform-es5-property-mutators/)を使えば変換はしてくれますが、
変換後に使用されるObject.defineProperty()のpolyfillがgetterやsetterに対応してません。

## 圧縮の設定
圧縮の設定で重要なのは、UglifyJsPluginに渡すオプションです。

```js:webpack.config.js
plugins: [
  new webpack.optimize.OccurenceOrderPlugin(),
  new webpack.optimize.DedupePlugin(),
  new webpack.optimize.UglifyJsPlugin({
    compress: {
      screw_ie8: false,
    },
    mangle: {
      screw_ie8: false,
    },
    output: {
      screw_ie8: false,
      ascii_only: true
    }
  }),
  new webpack.optimize.AggressiveMergingPlugin(),
],
```

UglifyJS 2が2.7以降の場合(webpack 1.13.3以降を使用する場合)は、`screw_ie8`を`compress`、`mangle`、`output`で`false`に設定して、IE8のサポートを有効にします。
UglifyJS 2が2.6系の場合(webpack 1.13.2までを使用する場合)は、IE8のサポートがデフォルトで有効なので設定しなくても問題ないです。
IE8のサポートが有効な場合、予約語のプロパティは変換しないようにしてくれます。

また、`output`では`ascii_only`を`true`にしています。
Jscriptはユニコードをうまく扱うことができないため、asciiで出力するようにします。

## おわりに

ES2015までの多くの機能が使えるようになりました。
もともとがES3なので、polyfillではカバーできない部分もあると思うので、エラーが発生するメソッドや構文などもあるかと思います。
その点に注意しながらコードを書くといいと思います。

## 参考
* [JScript5.8をES5化してバッチに利用するのだ - ペペロン頭脳](http://peperon-brain.hatenablog.com/entry/2015/06/22/003017)