Webpack 入門 その3 : Babel による ECMAScript のトランスパイル・TypeScript のトランスパイル

Webpack を勉強しながら Browserslist の効果を確認するシリーズ企画第3弾。

今回は初めに ECMAScript を Babel でトランスパイルしつつ、Browserslist の効果を確認。さらにその後、TypeScript もトランスパイルできるようにしてみる。

目次

使用するツールと適用順

ECMAScript のトランスパイルに使用するのは、おなじみ Babel である。

コアである @babel/core と、Polyfill を提供する core-js、ターゲットブラウザなどを判定する @babel/preset-env、そしてそれを Webpack で読み込むための babel-loader が必要になる。

続いて TypeScript をトランスパイルするにあたって必要なのは、typescript 本体と、ts-loader となる。

前回 SCSS をトランスパイルする際、CSS ファイルも sass-loader にかませていたのに気付いただろうか。

{
  // ↓ この正規表現は .sass・.scss・.css ファイルに該当する
  test: (/\.(sa|sc|c)ss$/u),
  // test: [ (/\.sass$/u), (/\.scss$/u), (/\.css$/u) ] と書くのと同じ
  use: [
    MiniCssExtractPlugin.loader,
    'css-loader',
    'postcss-loader',
    'sass-loader'
  ]
}

素の CSS は当然 SCSS 記法として認識させても素通りするだけで、その後の PostCSS がうまく効くというワケだ。

ECMAScript と TypeScript を混在させることは少ないだろうが、今回の素振り環境では、混在させてもうまく動くよう、似たようなことをやろうと思う。すなわち、

  1. ts-loader を使って TypeScript をトランスパイルする
    • 素の ECMAScript は素通り
    • トランスパイル処理自体は typescript パッケージが行う
  2. babel-loader を通じて Babel が ECMAScript をトランスパイルする
    • トランスパイル処理は @babel/core が担う
    • どの Polyfill を適用するかは、@babel/preset-env が Browserslist 設定を参照しながら決める
    • 実際に適用される Polyfill は core-js のコードが利用される

このような処理順となるワケだ。

必要なパッケージをインストールする

というワケでパッケージのインストール。

# ECMAScript のトランスパイルに必要なモノたち
$ npm install -D @babel/core @babel/preset-env core-js babel-loader

# TypeScript をトランスパイルする際はコレも追加する
$ npm install -D typescript ts-loader
"devDependencies": {
  "@babel/core": "7.10.5",
  "@babel/preset-env": "7.10.4",
  "babel-loader": "8.1.0",
  "core-js": "3.6.5",
  
  "ts-loader": "8.0.1",
  "typescript": "3.9.7",
  
  "webpack": "4.44.0",
  "webpack-cli": "3.3.12"
}

ECMAScript をトランスパイルしてみる

まずは ECMAScript のトランスパイルから試してみよう。

const wait = (ms) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve();
    }, ms);
  });
};

wait(1000)
  .then(() => {
    console.log('Test');
  });
// .babelrc は JSON だがコメントが書ける
{
  "presets": [
    [
      "@babel/preset-env",
      {
        // usage にすると Browserslist の指定に応じて Polyfill を入れられる
        "useBuiltIns": "usage",
        // Polyfill のために使用する core-js のバージョンを指定する
        "corejs": 3
      }
    ]
  ]
}
> 5%
last 2 versions
Firefox ESR
dead

# IE に対応させる
ie >= 6
const path = require('path');

module.exports = {
  mode: 'production',
  entry: './src/js/main.js',
  output: {
    path    : path.resolve(__dirname, 'dist/'),
    filename: 'js/[name].js'
  },
  module: {
    rules: [
      {
        test: (/\.js$/u),
        use: [
          'babel-loader'  // オプションは .babelrc で指定する
        ],
        exclude: (/node_modules/u)
      },
    ]
  }
};

コレでビルドしてみよう。すると ./dist/js/main.js の中に Promise 関連の Polyfill が含まれていることが何となく確認できると思う。

別ファイルのインポートやエクスポートにも、Webpack がデフォルトで対応している。

const another = 'another.js';
module.exports = another;
const another = require('./another');
console.log(another);

こんな風に2ファイル用意しても、うまくビルドできる。

TypeScript をトランスパイルする

続いて、TypeScript をトランスパイルできるようにする。

import child from './child';  // TypeScript ファイル
import * as another from '../js/another';  // ECMAScript ファイル

const wait = (ms: number = 1000) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve();
    }, ms);
  });
};

const waitMs: number = 2000;
wait(waitMs)
  .then(() => {
    console.log(child);
    console.log(another);
  });
const child: string = 'child.ts';
export default child;
const another = 'another.js';
module.exports = another;
{
  "compilerOptions": {
    "target": "esnext",
    "module": "esnext",
    "moduleResolution": "node",
    // JavaScript ファイルを読み込むために指定する
    "allowJs": true,
    // VSCode 上の「入力ファイルを上書きすることになるため、src/another.js を書き込めません。」エラーを回避するために宣言のみ記載・Webpack がファイル出力するため使用されない
    "outDir": "",
    
    // 以下テキトーに設定…
    
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    
    "esModuleInterop": true
  },
  "include": ["src/"],
  "exclude": ["node_modules/"]
}
const path = require('path');

module.exports = {
  mode: 'production',
  entry: './src/ts/main.ts',  // エントリポイントを TypeScript に直す
  output: {
    path    : path.resolve(__dirname, 'dist/'),
    filename: 'js/[name].js'
  },
  resolve: {
    // デフォルトでは拡張子 .ts を取得できないため指定する (import の解決などに使用する)
    extensions: ['.ts', '.js']
  },
  module: {
    rules: [
      {
        test: [(/\.ts$/u), (/\.js$/u)],
        use: [
          'babel-loader',  // オプションは .babelrc で指定する
          'ts-loader'      // tsconfig.json も参照
        ],
        exclude: (/node_modules/u)
      },
    ]
  }
};

resolve セクションで .ts ファイルを解釈できるようにしておく。test 部分は (/\.(j|t)s$/u) と表現しても良いが、.ts.js を対象にしている。

…ふぅ…。設定ファイル多すぎない?w

コレでビルドしてみると、ちゃんとコードが動くことが確認できる。

$ npm start

> practice-browserslist@ start /Users/Neo/practice-browserslist
> npm run build && node ./dist/js/main.js

> practice-browserslist@ build /Users/Neo/practice-browserslist
> webpack

Hash: 8cef69703bd99dbae1b4
Version: webpack 4.44.0
Time: 1929ms
Built at: 2020-07-28 14:24:52
     Asset      Size  Chunks             Chunk Names
js/main.js  18.7 KiB       0  [emitted]  main
Entrypoint main = js/main.js
[40] ./src/js/another.js 68 bytes {0} [built]
[42] (webpack)/buildin/global.js 472 bytes {0} [built]
[78] ./src/ts/main.ts + 1 modules 760 bytes {0} [built]
     | ./src/ts/main.ts 695 bytes [built]
     | ./src/ts/child.ts 45 bytes [built]
    + 76 hidden modules

# Promise による待機のあと、以下が出力される
child.ts
another.js

できたできた。

SCSS のトランスパイルコードと合わせてまとめ

前回の設定もまとめたモノを再掲しよう。

これらは省略する。

import '../css/other.css';   // CSS
import '../scss/main.scss';  // SCSS

import * as another from '../js/another';  // ECMAScript
import child from './child';               // TypeScript
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
  mode: 'production',  // モード指定
  entry: './src/ts/main.ts',  // エントリポイント
  // 出力先パス・ファイル名
  output: {
    path    : path.resolve(__dirname, 'dist/'),
    filename: 'js/[name].js'
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: '/css/[name].css'
    })
  ],
  resolve: {
    // デフォルトでは拡張子 .ts を取得できないため指定する (import の解決などに使用する)
    extensions: ['.ts', '.js']
  },
  module: {
    rules: [
      // ECMAScript をトランスパイルする
      {
        test: [(/\.ts$/u), (/\.js$/u)],
        use: [
          'babel-loader',  // オプションは .babelrc で指定する
          'ts-loader'      // tsconfig.json も参照
        ],
        exclude: (/node_modules/u)
      },
      // SCSS を CSS ファイルとして出力する
      {
        test: (/\.(sa|sc|c)ss$/u),
        use: [
          // Loader は最後のモノから順に適用される
          // sass-loader で SCSS から CSS に変換 → postcss-loader (PostCSS) で Autoprefixer を適用し CSS 圧縮 → css-loader で @import などを解決 → mini-css-extract-plugin で CSS ファイルとして書き出す
          MiniCssExtractPlugin.loader,
          'css-loader',
          'postcss-loader',  // Autoprefixer の指定は postcss.config.js で行う
          'sass-loader'
        ]
      }
    ]
  }
};

こうなった。

以上

設定多すぎてしんどい!Webpack の設定は自分で書くもんじゃない気がする…。

Browserslist の効果はなかなか面白い。一つの設定ファイルで Autoprefixer (CSS) にも Babel (JS) にも効果が出るので、CSS と JS のブラウザサポートの範囲を統一できる。

つらみはあったけど、コレで Browserslist および Webpack の扱いはおけおけですわ。