🚅

gulpとwebpackを使っていたビルド処理をViteに移行した

2024/12/25に公開

少し前になるのですが、gulpとwebpackで組んでいたビルド処理をVite(とnpm scripts)に置き換えたので、その経緯や結果をまとめました。

前提条件

まずは今回の対象プロジェクトのビルドまわりの環境や構成について紹介します。

  • Node.js 16
  • TypeScript非対応
  • gulp v4.0.2
  • webpack v5.52.1
  • ビルド結果はUMDモジュールとして出力する
  • エントリーファイルが複数あり、複数のモジュールを出力する

使っていたgulpのプラグインには以下のようなものがあります。

  • gulp-uglify
  • gulp-clean-css
  • gulp-filter
  • gulp-rename
  • gulp-html-replace
  • gulp-zip
  • gulp-plumber
  • gulp-install

なぜ置き換えることにしたのか

ありきたりですが、最大の理由はビルドに無視できないほどの時間がかかっていたためです。
本番用のビルドはおおよそ1分、開発環境での watch ビルドで変更が反映されるのにも10秒近くかかっていました。

別の理由として、gulpfileや自前のプラグインなどの作り込みが多く、秘伝のタレ化しつつあったことも保守性の観点での懸念でした。

あと、これはそこまで大きな理由ではないのですが、gulpもwebpackも今度の継続的なアップデートが見込めなさそうと判断したことも理由の一つです。gulpについては(2024年にメジャーアップデートがありましたが)約5年更新がなかったですし、webpackについても後継のTurbopackに開発が移っておりwebpack自身は事実上開発終了しているようです。

なぜViteか

開発チームの中でVue + Viteの組み合わせがこなれてきたこともあって、ツールチェインを揃えたかったという思いからViteを採用しました。
要件を満たせており、他での採用事例も多くWeb上でのノウハウも豊富なことも採用理由の一つです。

Webpackの開発者が移籍して作られた後継となるTurbopack / Turboや、webpackとの互換性を重視したRspackなど、複数の選択肢があり、どれがベストかという確証は今もありません。
ただ今回対応した内容は、将来的に別のツールに乗り換える時にも無駄にはならないと思うので、一つの区切りとしてViteは適切な選択だったのかと考えています。

変更の流れ

行ったことをおおよその時系列で記載しておきます。

Node v18+への対応

Viteを使うためにはNode.js 18+または20+のバージョンが必要であり、Node.jsのバージョンを上げるところから始めました。
単純にランタイムを上げただけではエラーになります。こちらはwebpackで使っているOpenSSLの互換性の問題でした。

node:internal/crypto/hash:68
  this[kHandle] = new _Hash(algorithm, xofLen);
                  ^

Error: error:0308010C:digital envelope routines::unsupported
    at new Hash (node:internal/crypto/hash:68:19)
    at Object.createHash (node:crypto:138:10)
  ...(略)...
  opensslErrorStack: [ 'error:03000086:digital envelope routines::initialization error' ],
  library: 'digital envelope routines',
  reason: 'unsupported',
  code: 'ERR_OSSL_EVP_UNSUPPORTED'
}

webpackは最終的に削除する予定だったため、ワークアラウンドとして NODE_OPTIONS--openssl-legacy-provider オプションを指定し、暫定的に対処しました。

package.json
-    "build": "gulp build",
+    "build": "NODE_OPTIONS=--openssl-legacy-provider gulp build",
-    "test": "gulp test",
+    "test": "NODE_OPTIONS=--openssl-legacy-provider gulp test",
-    "dev": "run-p dev:*",
+    "dev": "NODE_OPTIONS=--openssl-legacy-provider run-p dev:*",
-    "dev:server": "webpack-dev-server --config ./webpack-dev.config.js",
+    "dev:server": "NODE_OPTIONS=--openssl-legacy-provider webpack-dev-server --config ./webpack-dev.config.js",

Vite, TypeScriptをインストール

次は、ViteとTypeScriptを同じプロジェクトで利用できるようにすることを目指しました。

別の作業スペースで、以下を実行し、クリーンなViteプロジェクトを生成します。

$ npm create vite@latest
> npx
> create-vite

✔ Project name: … hello-vite
✔ Select a framework: › Vue
✔ Select a variant: › TypeScript

ここで生成した以下のファイルを対象リポジトリにコピーすることで、対応します。

  • vite.config.js
  • tsconfig.json
  • tsconfig.app.json
  • tsconfig.node.json

既存のプロジェクトに npm i -D vite typescript をしなかったのは、プロジェクトの構成や設定ファイルもなるべくスタンダードなものに合わせたかったからです。

ローカル開発サーバを起動するコマンドについては、元のものはそのまま残し、Viteのものは別のタスクとして定義しました。しばらくは両方が使える状態で開発・修正する必要があるためです。

package.json
    "dev": "run-p dev:*",
    "dev:server": "webpack-dev-server --config ./webpack-dev.config.js",
    "dev:watch": "gulp watch:html",
+   "vite": "vite",

run-p は元々入れていた並列実行するためのnpm-run-allモジュールです。

この状態ではViteのエントリーポイントである index.html からは既存の資産が参照されませんから、 npm run vite も動く状態です。
対象モジュールは複数あるので、段階的にVite化を進める準備が整いました。複雑なことをしていない単純なモジュールから、順次Vite化を進めていくことができます。

複数エントリーポイントへの対応 = それぞれ vite.config.xxx.js を用意する

ビルドファイルを複数用意する必要がありましたが、生成したいビルドファイル単位(≒静的リソース)で vite.config.xxx.js を作ることで対応しました。

Viteでは複数エントリーを指定することはできるので、1つのファイルで対応することは本来できそうなのですが、分けた方が色々と潰しが効いて管理しやすいと判断し、今の形に落ち着きました。
1つ1つの設定ファイルは概ね以下のようなものになっています。

vite.config.foo.js
import { resolve, join } from 'path';
import { defineConfig } from 'vite';
import inject from '@rollup/plugin-inject';

const root = resolve(__dirname, 'app');
// 静的リソースの置き場所を出力先に直接指定する
const outDir = resolve(__dirname, 'force-app/main/default/staticresources/Foo');

export default defineConfig({
  base: './',
  // publicDir にあるものも全てアセットファイルとして出力(コピー)されるので、不要にしておく
  publicDir: false,
  root,
  build: {
    lib: {
      entry: {
        'foo': resolve(root, 'js/foo.js')
      },
      // 互換性を保つためwebpackの時のモジュール名と合わせる
      name: 'MyModule',
      formats: ['umd']
    },
    outDir,
    emptyOutDir: true,
    rollupOptions: {
      output: {
        // ファイル名の枝番にハッシュが出力されないように明示的に指定
        entryFileNames: 'js/[name].js',
        chunkFileNames: `js/[name].js`,
        assetFileNames: `css/[name].[ext]`
      }
    }
  },
  plugins: [
    inject({
      $: 'jquery',
      jQuery: 'jquery'
    })
  ]
});

古いコードでjQueryが使われていたので @rollup/plugin-inject で使えるようにしています。

環境変数の扱いを修正

Viteでは環境変数の扱いが異なるため、以下のように変更します。

- if (process.env.NODE_ENV === 'production') {
+ if ((typeof process !== 'undefined' && process.env.NODE_ENV === 'production') || import.meta.env.PROD) {

Viteでは環境変数はimport.meta.envオブジェクトを通じて利用します。
import.meta.env.PROD はプロダクションモードかを判定する組み込みの環境変数です。
幸いなことに(?)今回のプロジェクトでは、環境変数はあまり使用していなかったので、上記だけで済みました。独自の環境変数を使用する場合はVITE_プレフィックスを使って利用する必要があります。

CommonJS (CJS) → ESM 形式に変更

Viteを使う場合はCJSが非推奨になっているため、ESM形式に変更していきます。
ほぼ機械的な作業ですが、修正量が一番大きいのはこの作業でした。

- const foo = require('foo');
+ import foo from 'foo';
- require('./foo.css');
+ import './foo.css';
- const foo = require('foo').foo;
+ import { foo } from 'foo';

モジュールの読み込み方法を変更する

ローカル開発におけるHTML側も修正します。
ViteではNative ESModulesのimportを利用し、ブラウザから直接モジュールを読み込めるので、以下のようなイメージで変更します。

- <script src="./js/hoge-main.js"></script>
- <script>
-     MyModule.hoge.run({ ... })
- </script>
+ <script type="module">
+     import { hoge } from './js/hoge';
+     hoge.run({ ... });
+ </script>

package.json のビルド周りを修正

npm run vite:build で必要なファイルが全てビルドできるように package.json を以下のように修正しました。

(省略)
  "type": "module",
  "scripts": {
    "vite": "vite",
    "vite:build": "run-p vite:build:foo vite:build:bar vite:build:baz",
    "vite:build:foo": "vite build -c vite.config.foo.js",
    "vite:build:bar": "vite build -c vite.config.bar.js",
    "vite:build:baz": "vite build -c vite.config.baz.ts",
(省略)

依存先のモジュールがCJS形式で module is not defined になる場合のワークアラウンド

一部の利用ライブラリがESMに対応していなかったりで、エラーになる場合があります。

別の実装手段に切り替えるのは後回しとして、ワークアラウンドとして例外的にvite-plugin-commonjsを使ってフィルターすることで対応しました。

vite.config.xxx.js
import commonjs from 'vite-plugin-commonjs';
// 略
export default defineConfig({
  // 略
  plugins: [
    inject({
      $: 'jquery',
      jQuery: 'jquery'
    }),
    // 追加
    commonjs({
      filter(id) {
        if (id.includes('node_modules/jquery-inview')) {
          return true;
        }
      }
    })
  ]
// 略

gulp, webpackへの依存関係の削除とgulpfile, webpack.config.jsの削除

一通りの移行作業が終わったらgulp, webpackの依存関係を削除し、gulpfile , webpack.config.js も削除します。

おわりに

新旧の比較は以下です。

gulp + webpack Vite
ホットリロード 約10秒 一瞬
本番ビルドの時間 約1分 約7〜8秒

今回のマイグレーションで大変だったのは以下の2つでした。

  • CJSからESMへの変更
  • ESMにするにあたって相性の悪い・動かないライブラリについて、代替実装やワークアラウンドによる解消(個別の対応が多かったので本記事では割愛しています)

数が多かったので大変でしたが、大きな問題もなく無事に移行完了できました。

こういったモダナイゼーションの取り組みは、ありふれた話題ではあるのですが、使っているgulpのプラグインやビルドにおける作り込みなどはプロジェクトごとに異なり、苦労するポイントや対応なども様々かと思うので、もし何かの参考になれば幸いです。

株式会社キャリオット

Discussion