かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

Gulp + Rollup で webサイト用の JS をビルドしたい

10年くらい前に作った WEB サイトの改修案件が発生して、構成が古すぎて手に負えなくなっていたのでコレを気にビルド環境を見直しました。
元のものが Grunt で JS を結合したり SCSS をビルドしている構成だったので Grunt を Gulp に置き換え、SCSS のビルドはそのまま JS は順番を決めて結合するのではなくバンドルするようにしたい。Vite のビルドに Rollup が使われていたので webpack ではなく Rollup を使う構成にしてみました。(JS だけなら Gulp を使う必然性がないのですが SCSS の微都度と一緒に watch して JS, CSS どりらでもビルドできるという理由で導入しました)

Rollup で JS をビルドできる環境を作る

install packages

$ npm i -D rollup
# Babel 関連
$ npm i -D @babel/core @babel/preset-env @rollup/plugin-babel
# npm install したパッケージをバンドルするためのプラグイン
$ npm i -D @rollup/plugin-node-resolve
note

設定ファイル

.babelrc.json

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "loose": true,
        "targets": "> 1%, not dead",
        "modules": false
      }
    ]
  ]
}

module: false でないと Rollup のエラーになる

rollup.config.js

// npm modules を bundle するためのプラグイン
import { nodeResolve } from '@rollup/plugin-node-resolve';
// babel
import { babel } from '@rollup/plugin-babel';

const config = {
  input: './src/index.js'
  output: {
    file: './build/assets/js/main.js',
    // Web サイトでの利用なので即時関数で出力される iife を指定
    format: 'iife',
  }
  plugins: [
    nodeResolve(),
    babel({ babelHelpers: 'bundled' }),
  ],
};

export default config;

cf. format に関して こちらの記事 がわかりやすかったです

Rollup でバンドルできているかのチェック

$ npx rollup --config

上記コマンドを実行しビルドされた JS ファイルが出力されていれば OK

Gulp から Rollup を実行する

$ npm i -D gulp
$ touch gulpfile.js

gulp の設定ファイルを作成

gulpfile.js

'use strict';
const { watch, parallel, series } = require('rollup');
const { rollup } = require('rollup');
const config = require('./rollup.config');

const buildJS = async (cb) => {
  await rollup(config)
    .then(async (bundle) => {
      // ファイルの生成も非同期なので生成完了まで await する
      await bundle.write(config.output);
      Promise.resolve();
    })
    .catch((error) => {
      if (cb) {
        cb();
      }
      console.error(error);
    });

  cb();
};

const buildJSTask = series(buildJS);

exports.default = () => {
  watch(['./src/**/*.js'], buildJSTask);
};
exports.build = parallel(buildJSTask);

直接 Rollup を呼び出す方法を採用しました。
npx gulp でファイル監視, npx gulp build でビルドコマンドが実行されます。

cf. Webフロントエンド - GulpでRollupを使う / rollup-stream、rollup | ブログ | STUDIO BUS STOP | 香川県高松市

rollup.config.js を CommonJS の形式に修正する

gulp が CommonJS の形式なので rollup.config.js 内で import / export を使用しているとエラーになってしまいます。 gulpfile 内で import / export を使えるようにする方法もあるが、gulp のプラグインなど対応してないものがあると面倒そうなので rollup.config.js を CommonJS 形式に書き換えます。

rollup.config.js

- import { nodeResolve } from '@rollup/plugin-node-resolve';
+ const { nodeResolve } = require('@rollup/plugin-node-resolve');
- import { babel } from '@rollup/plugin-babel';
+ const { babel } = require('@rollup/plugin-babel');

const config = {
  input: './src/index.js'
  output: {
    file: './build/assets/js/main.js',
    format: 'iife',
  }
  plugins: [
    nodeResolve(),
    babel({ babelHelpers: 'bundled' }),
  ],
};

- export default config;
+ module.exports = config;

npm script の作成

gulp を実行する npm script を作成

package.json

{
  "scripts": {
    "gulp:build": "npx gulp build",
    "gulp:dev": "npx gulp"
  },
}

npm run gulp:build を実行して JS のビルドができれば OK


Options

minifiy

gulp で行なうほうが十分に枯れている気もしますが、Rollup のプラグインは minify したものとして無いものを別々に出力できそうだったのと後述の console.log の削除が Rollup で行った方が良さそうだったので Rollup のプラグインを採用しました。

$ npm i -D rollup-plugin-terser

rollup.config.js

const { nodeResolve } = require('@rollup/plugin-node-resolve');
const { babel } = require('@rollup/plugin-babel');
const  { terser } = require('rollup-plugin-terser');

const config = {
  input: './src/index.js'
  output: [
    {
      file: './build/assets/js/main.js',
      format: 'iife',
    }, 
    {
      file: '/build/assets/js/main.min.js',
      format: 'iife',
      plugins: [terser()],
    },
  ]
  plugins: [
    nodeResolve(),
    babel({ babelHelpers: 'bundled' }),
  ],
};

module.exports = config;

rollup-plugin-terser プラグインは sourcemap を出力できない cf. Allow configurable sourcemaps. · Issue #64 · TrySound/rollup-plugin-terser · GitHub


console.log の削除

min 化したファイルからは console.log を取り除きたい

gulp の方は開発がかなり前に止まっているし Star も少ないので Rollup のプラグインを使うのが良さそうです。

@rollup/plugin-strip プラグインは output 内の plugin として使えないようです

rollup.config.js

const { nodeResolve } = require('@rollup/plugin-node-resolve');
const { babel } = require('@rollup/plugin-babel');
const  { terser } = require('rollup-plugin-terser');
+ const strip = require('@rollup/plugin-strip');

const config = {
  input: './src/index.js'
  output: [
    {
      file: './build/assets/js/main.js',
      format: 'iife',
    }, 
    {
      file: '/build/assets/js/main.min.js',
      format: 'iife',
-     plugins: [terser()],
+     plugins: [terser(), strip()],
    },
  ]
  plugins: [
    nodeResolve(),
    babel({ babelHelpers: 'bundled' }),
  ],
};

module.exports = config;

上記の様に output 内の plugin で使用しようとしても正しく動作しません 👇

$ npx npx rollup --config
./src/index.js → ./build/assets/js/main.js, ./build/assets/js/main.min.js...
(!) The "transform" hook used by the output plugin strip is a build time hook and will not be run for that plugin. Either this plugin cannot be used as an output plugin, or it should have an option to configure it as an output plugin.
created ./build/assets/js/main.js, ./build/assets/js/main.min.js

出力された main.min.jsconsole.log は削除されず残ったまま…
どうやら @rollup/plugin-strip は全体の plugin としてしか動作しないようです。

production / development で場合分けをする

Rollup は --environment <values>環境変数を渡せるので、これを利用する

rollup.config.js

const { nodeResolve } = require('@rollup/plugin-node-resolve');
const { babel } = require('@rollup/plugin-babel');
const  { terser } = require('rollup-plugin-terser');
const strip = require('@rollup/plugin-strip');

const isProduction = process.env.MODE === 'production' || false;
const input = './src/index.js';
const outputFile = isProduction ? '/build/assets/js/main.min.js' : './build/assets/js/main.js';
const format = 'iife';
const plugins = [
  nodeResolve(),
  babel({ babelHelpers: 'bundled' }),
];

const config = () => {
  if (isProduction) {
    return {
      input,
      output: {
        file: outputFile,
        format,
        plugins: [terser()],
      },
      plugins: [...plugins, strip()],
    };
  } else {
    return {
      input,
      output: {
        file: outputFile,
        format,
      },
      plugins: [...plugins],
    };
  }
};

module.exports = config();

npm script

{
  "scripts": {
    "build:dev": "npx rollup --config",
    "build:prod": "npx rollup --config --environment MODE:production",
    "build": "npm run build:dev && npm run build:prod"
  },
}

👇

$ npm run build
> npx rollup --config
./src/index.js → ./build/assets/js/main.js...
created ./build/assets/js/main.js
> npx rollup --config --environment MODE:production
./src/index.js → ./build/assets/js/main.min.js...
created ./build/assets/js/main.min.js

build が 2回実行されてしまいますが、minify する際のみ console.log を取り除くことができました!

gulp からの呼び出し

gulp から呼び出す場合 Rollup の --environment <values> をコマンドから渡すことができません。また gulp 内で使っている rollup メソッドは設定ファイルを引数に取るので環境変数を渡すことはできなそうです。

export function rollup(options: RollupOptions): Promise<RollupBuild>;

export interface RollupOptions extends InputOptions {
  // This is included for compatibility with config files but ignored by rollup.rollup
  output?: OutputOptions | OutputOptions[];
}

gulp から呼び出す場合は process.env.NODE_ENV を gulp に渡して rollup メソッドに渡す設定ファイルを変更するのが良さそうです

方針
  1. NODE_ENV=production npx gulp build => minify する Rollup の設定を使う
  2. npx gulp build => minify しない Rollup の設定を使う
/root
  |- /rollup
  |    |- config.common.js # 共通設定
  |    |- config.dev.js # development用の設定
  |    |- config.prod.js # production用の共通設定
  |- gulpfile.js

Rollup の設定を分割する

config.common.js

require { nodeResolve } = require('@rollup/plugin-node-resolve');
require { babel }  = require('@rollup/plugin-babel');

exports.input = './src/index.js';
exports.outputDir = './build/assets/js';
const outputFile = `${outputDir}/main.js`;
exports.output = {
  file: outputFile,
  // esm, cjs, amd, system, iife, umd
  format: 'iife',
};
exports.plugins = [nodeResolve(), babel({ babelHelpers: 'bundled' })];

config.dev.js

const { input, output, plugins } = require('./config.common)';

const config = {
  input,
  output: {
    ...output,
  },
  plugins: [...plugins],
};

module.exports = config;

config.prod.js

const { terser } = require('rollup-plugin-terser');
const strip = require('@rollup/plugin-strip');
const { input, output, outputDir, plugins } = require('./config.common');

const config = {
  input,
  output: {
    ...output,
    file: `${outputDir}/main.min.js`,
    plugins: [terser()],
  },
  plugins: [
    ...plugins,
    strip({
      labels: ['unittest'],
    }),
  ],
};

module.exports = config;

gulpfile の設定を修正する

gulpfile.js

'use strict';
const { watch, parallel, series } = require('rollup');
const { rollup } = require('rollup');
- const config = require('./rollup.config');
+ // 場合分けして Dynamic import もできると思うが複雑になりそうなので試さない
+ const configDev = require('./rollup/config.dev.mjs');
+ const configProd = require('./rollup/config.prod.mjs');
+ const isProduction = process.env.NODE_ENV === 'production' || false;
+ const config = isProduction ? configProd : configDev;

const buildJS = async (cb) => {
  await rollup(config)
    .then((bundle) => {
      bundle.write(config.output);
    })
    .catch((error) => {
      if (cb) {
        cb();
      }
      console.error(error);
    });

  cb();
};

const buildJSTask = series(buildJS);

exports.default = () => {
  watch(['./src/**/*.js'], buildJSTask);
};
exports.build = parallel(buildJSTask);

gulp を実行する npm script

package.json

{
  "scripts": {
+   "gulp:build:dev": "npx gulp build",
+   "gulp:build:prod": "NODE_ENV=production npx gulp build",
+   "gulp:build": "npm run gulp:build:dev && npm run gulp:build:prod",
+   "gulp:dev": "npx gulp",
+   "gulp:dev:prod": "NODE_ENV=production npx gulp",
    "build:dev": "npx rollup --config",
    "build:prod": "npx rollup --config --environment MODE:production",
    "build": "npm run build:dev && npm run build:prod"
  },
}

npm run gulp:build を実行して main.js と minify され console.log が取り除かれた main.min.js ができていればOK


gizip

どっちもどっちと言う感じです。CSSgzip 化するのであれば gulp のプラグインにすればインストールするパッケージが 1 つで済みそうです

Rollup で gzip

$ npm i -D rollup-plugin-gzip
# Brotil 化
$ npm i -D zlib util

rollup.config.js

const { nodeResolve } = require('@rollup/plugin-node-resolve');
const { babel } = require('@rollup/plugin-babel');
const  { terser } = require('rollup-plugin-terser');
const strip = require('@rollup/plugin-strip');
+ const gzipPlugin = require('rollup-plugin-gzip');
+ const { brotliCompress } = require('zlib');
+ const { promisify } = require('util');

const isProduction = process.env.MODE === 'production' || false;
const input = './src/index.js';
const outputFile = isProduction ? '/build/assets/js/main.min.js' : './build/assets/js/main.js';
const format = 'iife';
const plugins = [
  nodeResolve(),
  babel({ babelHelpers: 'bundled' }),
];

const config = () => {
  if (isProduction) {
    return {
      input,
      output: {
        file: outputFile,
        format,
-       plugins: [terser()],
+       plugins: [
+         terser(),
+         gzipPlugin({
+           fileName: '.gz',
+         }),
+         // Brotil compression as .br files
+         gzipPlugin({
+           customCompression: (content) => brotliPromise(Buffer.from(content)),
+           fileName: '.br',
+         }),
+       ],
      },
      plugins: [...plugins, strip()],
    };
  } else {
    return {
      input,
      output: {
        file: outputFile,
        format,
      },
      plugins: [...plugins],
    };
  }
};

module.exports = config();

gulp で gzip

$ npm i -D gulp-gzip
# Brotli 化
$ npm i -D gulp-brotli zlib util

gulpfile.js

'use strict';
const { watch, parallel, series, src, dest } = require('rollup');
const { rollup } = require('rollup');
+ const gzip = require('gulp-gzip');
+ const  gulpBrotli = require('gulp-brotli');

const configDev = require('./rollup/config.dev.mjs');
const configProd = require('./rollup/config.prod.mjs');
const isProduction = process.env.NODE_ENV === 'production' || false;
const config = isProduction ? configProd : configDev;

const buildJS = async (cb) => {
 /// 略
};

+ const destDir = './build/assets/js';
+ const gzipJS = (cb) => {
+   return src(`${destDir}/*.min.js`)
+     .pipe(gzip())
+     .pipe(dest(`${destDir}`));
+ };

+ const brotliJS = (cb) => {
+   return src(`${destDir}/main.min.js`)
+     .pipe(
+       gulpBrotli.compress({
+         extension: 'br',
+       })
+     )
+     .pipe(dest(`${destDir}`));
+ };

- const buildJSTask = series(buildJS);
+ const buildJSTask = isProduction
+   ? series(buildJS, parallel(gzipJS, brotliJS))
+   : series(buildJS);

exports.default = () => {
  watch(['./src/**/*.js'], buildJSTask);
};
exports.build = parallel(buildJSTask);

npm run gulp:build (npm run NODE_ENV=production npx gulp) を実行して main.min.js.gzipmain.min.js.br が生成されていればOK


所管

ココまで書いてて何だけど、gulp は総じて開発が止まっている印象ですし、CommonJS とかで結構めんどくさいから Gulp を使わない方針にするか Rollup 諦めて webpack でゴリッとやってしまったほうが楽だったかも知れない… (cf. webpack 5 CSS だけコンパイルしたい - かもメモ) Rollup についての知見をつけることができたのでよし!


[参考]