gulpfile.esm.js で gulp-imagemin v8.0.0 を利用するとエラーになる
- 公開日
- タグ
- gulp
- JavaScript
- Node.js
Node.js は v12 以降なら ESM が使えるが、gulp v4 もルートタスクファイルの名前を gulpfile.esm.js
とし、devDependencies に esm モジュールを追加する(npm i -D esm
)ことで、gulp タスクで ESM の import/export 構文が使えるようになる。
Transpilation - JavaScript and Gulpfiles | gulp
これで意気揚々と gulp-imagemin v8.0.0 を import するとエラーになる。
この記事は「gulp で ESM を使いたいが gulp-imagemin v8.0.0 でエラーが出た人」をターゲットに書いており、「gulp は CommonJS 構文のままで gulp-imagmin を v8.0.0 にあげたらエラーが出たが、gulp を ESM にしたくない人」向けの内容ではない。
後者の人への解決策としては、「gulp-imagemin v7.1.0 を使う」以外はないと思われる。
目次
解決方法
3つある。繰り返すが「gulp を ESM で利用したい、かつ gulp-imagemin を利用したい」人向けの解決策である。
gulp-imagemin を v7.1.0 にダウングレードする
gulp-imagemin を v7.1.0 に下げれば、gulpfile.esm.js
で import/export 構文のままエラーなく動作するようになる。
package.json に `"type": "module"` を追加する
package.json に "type": "module"
を追加すると、esm モジュールなしで import/export が使えて gulp-imagemin も v8.0.0 が動く。gulpfie.esm.js
へのリネームも不要だ。
なお、相対パスで import/export する場合は拡張子を省略することはできない。
// guilfile.js
import gulp from "gulp"; // `node_modules/` からは拡張子は省略可能
import { image } from "./task/image.js"; // 相対パスなので拡張子は省略不可
export default gulp.series(image);
注意点として、package.json に "type": "module"
があるとプロジェクト内で Node.js で実行されるファイル全てに ESM を要求するので、gulp タスク以外も ESM 化しなくてはならない。
タスクファイルの拡張子を全て `.mjs` にする
Node.js は拡張子が .mjs
のファイルを ESM で実行するようになっている。
なので、gulpfile.mjs
にリネームすることで、gulp-imagemin v8.0.0 でも動くようになる。esm モジュールは不要だ。タスクファイルを分割している場合、例えば画像処理に関するタスクファイルを task/image.js
としたら、そちらも task/image.mjs
とリネームしよう。
こちらも相対パスでの import/export の拡張子は省略できないが、"type": "module"
と違って CommonJS のまま使いたいスクリプトは .js
のままで動作する。
No gulpfile found エラーが出たら
gulp v4 に同梱されている gulp-cli が v2.3.0 になっていない可能性がある。
npx gulp
[00:00:00] No gulpfile found
npx gulp -v
CLI version: 2.2.0 // <= bad
Local version: 4.0.2
その場合は package-lock.json を削除してから npm i
し直すとよい。
rm -f package-lock.json
npm i
npx gulp -v
CLI version: 2.3.0 // <= good
Local version: 4.0.2
gulp-cli が .mjs
で ESM をサポートしたのは v2.3.0 からなので注意だ。
Release v2.3.0 · gulpjs/gulp-cli
比較
解決方法を一覧してみた。
gulp タスクを ESM 化するのでどの手段においても既存タスクファイルの require()
を import hoge from "hoge"
に変更する必要があるし、module.exports
を export
に変更する必要がある。
方法 | 必須作業 | 備考 |
---|---|---|
gulp-imagemin v7.1.0 を使う |
npm i -D esm と gulpfile.esm.js へのリネーム
|
ESM 化はできるが dependabot に警告されても gulp-imagemin を更新できない |
package.json に "type": "module" を追加する |
package.json へのオプション追加 | gulpfile.js のままで gulp-imagemin も v8.0.0 が使えるが gulp タスク以外の Node.js 互換スクリプトも ESM 化する必要がある |
タスクファイルの拡張子を全て .mjs にする |
対象ファイルの拡張子の変更 | 先進的。CommonJS のまま使いたい Node.js 互換スクリプトは変更しなくてよい |
それぞれのケースをまとめたリポジトリを作った
これを誰でも再現できるように、エラーが出る環境と、3つの解決方法を全てまとめたリポジトリを作ってみた。各ディレクトリで npm ci
が必要だが、手元で比べられるので参考にされたい。
エラーの再現手順とエラーログ
次のコマンドを1つずつ行なっていくとあなたにもエラーログが手に入る。
# お好みのディレクトリでどうぞ
mkdir awesome-project
cd awesome-project
npm init -y
touch gulpfile.esm.js
mkdir task
touch ./task/image.js
# 必要モジュールをインストールする
npm i -D esm gulp gulp-imagemin
package.json の devDependencis フィールドは次のようになっているだろう。
// package.json
{
...
"devDependencies": {
"esm": "^3.2.25",
"gulp": "^4.0.2",
"gulp-imagemin": "^8.0.0"
},
...
}
このうえで gulpfile.esm.js
を次のように、
// guilfile.esm.js, edit like below
import gulp from "gulp";
import { image } from "./task/image";
export default gulp.series(image);
./task/image.js
を次のようにする。
// ./task/image.js, edit like below
import gulp from "gulp";
import imagemin, { mozjpeg } from "gulp-imagemin";
export const image = () =>
gulp
.src("./src/image/**/*")
.pipe(imagemin([mozjpeg({ quality: 50 })]))
.pipe(gulp.dest("./dist/image/"));
サンプルということでシンプルにしている。これが!シンプルな!サンプル!
ということでプロジェクトルートで
npx gulp
を実行すると、
[01:23:45] Requiring external module esm
TypeError [ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING]: A dynamic import callback was not specified.
at new NodeError (node:internal/errors:371:5)
at importModuleDynamicallyCallback (node:internal/process/esm_loader:39:9)
at eval (eval at <anonymous> (/Users/awesome-project/node_modules/gulp-cli/lib/shared/require-or-import.js:10:15), <anonymous>:3:1)
at requireOrImport (/Users/awesome-project/node_modules/gulp-cli/lib/shared/require-or-import.js:24:7)
at execute (/Users/awesome-project/node_modules/gulp-cli/lib/versioned/^4.0.0/index.js:37:3)
at Liftoff.handleArguments (/Users/awesome-project/node_modules/gulp-cli/index.js:211:24)
at Liftoff.execute (/Users/awesome-project/node_modules/liftoff/index.js:201:12)
at module.exports (/Users/awesome-project/node_modules/flagged-respawn/index.js:51:3)
at Liftoff.<anonymous> (/Users/awesome-project/node_modules/liftoff/index.js:191:5)
at /Users/awesome-project/node_modules/liftoff/index.js:149:9 {
code: 'ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING'
}
TypeError [ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING]: A dynamic import callback was not specified.
エラーが出る。
さらにもう一度 npx gulp
を実行すると、
[02:34:56] Requiring external module esm
TypeError: Invalid host defined options
at eval (eval at <anonymous> (/Users/awesome-project/node_modules/gulp/node_modules/gulp-cli/lib/shared/require-or-import.js:10:15), <anonymous>:3:1)
at requireOrImport (/Users/awesome-project/node_modules/gulp/node_modules/gulp-cli/lib/shared/require-or-import.js:24:7)
at execute (/Users/awesome-project/node_modules/gulp/node_modules/gulp-cli/lib/versioned/^4.0.0/index.js:37:3)
at Liftoff.handleArguments (/Users/awesome-project/node_modules/gulp/node_modules/gulp-cli/index.js:211:24)
at Liftoff.execute (/Users/awesome-project/node_modules/liftoff/index.js:201:12)
at module.exports (/Users/awesome-project/node_modules/flagged-respawn/index.js:51:3)
at Liftoff.<anonymous> (/Users/awesome-project/node_modules/liftoff/index.js:191:5)
at /Users/awesome-project/node_modules/liftoff/index.js:149:9
at /Users/awesome-project/node_modules/v8flags/index.js:162:14
at /Users/awesome-project/node_modules/v8flags/index.js:41:14
TypeError: Invalid host defined options
エラーとなり、以降はずっと同じエラーになる。
Requiring external module esm
部分は正しく動作する場合でも表示されるので気にしなくてよい。
エラーログのスタックトレースを見てもタスクファイルのどこが悪いのかわからないが、手元のプロジェクトで色々検証した結果 gulp-imagemin v8.0.0 が原因だった。この構成で同じエラーが出るので間違いないだろう。
どうしてこんなエラーが出るのか
gulp-imagemin のリポジトリのイシューで「TypeError: Invalid host defined options」を検索するとドンピシャのものが出てくる。
TypeError: Invalid host defined options · Issue #362 · sindresorhus/gulp-imagemin
これにプラグインの作者が下記 gist へのリンクを貼り付けている。
いろいろ書かれているが、とりあえず「gulp-imagemin v8.0.0 はピュア ESM パッケージ」なのだという。ということは import/export を使うのは間違っているわけではなさそうだ。
では、esm モジュールをインストールして import/export が使えるようになったはずの gulpfile.esm.js が悪いのだろうか。
冒頭の gulp の Transpilation のリンクをよく見ると、
Most new versions of node support most features that TypeScript or Babel provide, except the
import
/export
syntax. When only that syntax is desired, rename togulpfile.esm.js
and install the esm module.
とある。
ほとんどの新しいバージョンのノードは、import/export 構文を除いて、TypeScript または Babel が提供するほとんどの機能をサポートしています。その構文のみが必要な場合は、
gulpfile.esm.js
にリネームして esm モジュールをインストールしてください。(筆者訳)
~ except the
のあとに import/export
syntax.When only that syntax is desired, ~
と続いているので、「import/export 構文のみが必要な場合は〜」と読める。
なるほど、esm モジュールには import/export のサポートしかないのかもしれない。それしかないので、ピュア ESM パッケージである gulp-imagemin が動かない、という可能性がある。
なぜエラーが出るのかの答えは「esm モジュールが期待と違いそうだった」となりそうだ。それ以上の原因究明には esm モジュールと gulp-imagemin プラグインのソースコードを両方読まなければならなそうでやめた。
gulp-imagemin に問題がないとわかれば、gulp を ESM で動作させる他の方法を検証すればよい。そうして記事前半の解決方法の提示に至った。
gulp や Node.js 互換のスクリプトを ESM に変更すべきか
すべきだろう。
Node.js が直ちに CommonJS のサポートをやめるとは思えないが、v12 で ESM をサポートしたのも JavaScript の標準仕様に倣うものであるし、いずれは ESM をデフォルトの実行環境とするのは想像に難くない。
今は .js
ファイルはデフォルトで CommonJS で実行されるが、どこかのタイミングで ESM になり、CommonJS で実行するほうがオプションになると予想する。
gulp-imagemin に限らず、他の gulp プラグインや Node.js プラグインもそう時を待たずに ESM 化していくだろう。
とは言え、CommonJS の require()
は JSON ファイルもパースしてくれるで便利だ。例えば、
// ./package.json
{
"name": "awesome-module",
"version": "0.1.0"
}
という JSON ファイルの version
を引き当てたいとき、CommonJS なら次のようにすれば動く。
// index.js(CommonJS)
const { version } = require("./package");
console.log(version); // => 0.1.0
これを ESM でやると次のようになる。
// index.mjs(ESM)
import { readFileSync } from "fs";
import { dirname, resolve } from "path";
import { fileURLToPath } from "url";
// Node.js 環境向けの置き換え
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const _package = readFileSync(resolve(__dirname, "./package.json"), "utf8");
console.log(_package.version); // => undefined
let parsedPackage;
try {
parsedPackage = JSON.parse(_package);
} catch (e) {
console.error(e);
}
console.log(parsedPackage.version); // => 0.1.0
このように、ESM の import
構文は JSON ファイルを読み込めないので fs.readFile()
や fs.readFileSync()
を使わなくてはいけない。
そして fs
モジュールはファイルシステムとしての I/O しかないので、それを JSON として扱うには JSON.parse()
を経由する必要がある。さらに JSON.parse()
はパースできないと例外を返すので try catch
もしなくてはならない。
こうした変更は ESM 化するなら避けられない作業なので頑張ろう。
あちこちで使うならユーティリティにしておこう。
// ./utilities/getParsedJSON.mjs
import { readFileSync } from "fs";
import { dirname, resolve } from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
export const getParsedJSON = (filepath) => {
try {
return JSON.parse(readFileSync(resolve(__dirname, filepath), "utf8"));
} catch (e) {
console.error(e);
}
};
// index.mjs
import { getParsedJSON } from "./utilities/getParsedJSON.mjs";
const parsedPackage = getParsedJSON("./package.json");
console.log(parsedPackage.version); // => 0.1.0
以上、gulpfile.esm.js
で gulp-imagemin v8.0.0 を利用するとエラーになる、だった。
gulp-imagemin 以外のピュア ESM パッケージでも同じエラーが起こる可能性があるわけで、こうなってしまうと gulpfile.esm.js
を使う理由はないに等しい。個人的には .mjs
を使う方法を推す。みんなも ESM しようぜ!
Special thanks @watilde!