運用に耐えた怪談のような
webpack plugin たち

2019-08-29 Build Battle Saga ~ Frontend ~

@tkdn

Who

  {
    "name"  : "Satoshi Takeda / 武田 諭",
    "social": "tkdn",
    "job"   : "Front-End web developer",
    "history": {
      "2003-2013" : "Actor",
      "2013-"     : "Developer"
    }
  }

自責の念にかられながら作った
禁忌の webpack plugin たち。

今日は自責を供養しに来ました。
積極的な使用はおすすめしません。

怪談 plugin
三銃士を連れて来たよ

  1. 💀 Es3ifyPlugin

    • ES3 相当にコンパイル…
  2. 💀 DefinePropertyPatchPlugin

    • 2011年くらいの V8 バグのためにパッチ…
  3. 💀 AllowMutateEsmExportsPlugin

    • ES modules の仕様を裏切る…

Es3ifyPlugin
ES3 相当にコンパイル…

このご時世に ES3 という

  • 予約語をプロパティに宣言すると死

    • foo.default
  • 配列, オブジェクトで末尾カンマすると死

    • ["foo", "bar",]

まあまあある

  • es3ify-loader
  • es3ify-webpack-plugin

が、webpack に追従してなかったりしてる。

作って運用に耐えるようにした

const es3ify = require("es3-safe-recast").compile;
const ConcatSource = require("webpack-sources/lib/ConcatSource");

module.exports = class Es3ifyPlugin {
  apply(compiler) {
    compiler.hooks.emit.tap("Es3ifyPlugin", compilation => {
      Object.keys(compilation.assets)
        .filter(file => /.js$/.test(file))
        .forEach(file => {
          const asset = compilation.assets[file];
          compilation.assets[file] =
            new ConcatSource(es3ify(asset.source(), {
                trailingComma: true,
            }));
        });
      }
    );
  }
}

実は要らなさそう

無駄骨に終わった 😇

new TerserPlugin({
  terserOptions: {
    ie8: true,
  },
}),

DefinePropertyPatchPlugin
2011年くらいの V8 バグのためにパッチ…

webpack バンドル後、不穏なエラー

Uncaught TypeError: Cannot assign to read only property '__esModule' of #<Object>

調べると 2011 年 V8 のバグ

  • Android 4.0.x の端末に存在

    • もちろん改修・アップデートはされていない
    • Object.defineProperty が壊れてる
    • プロパティが immutable になり値を書き換えできない

1530 - Defining function's 'prototype' property with Object.defineProperty sets different value and makes property immutable - v8 - Monorail

webpack が ES modules を
エミュレートするところで
このバグを踏んでいるっぽい。

// webpack bootstrap
Object.defineProperty(exports, '__esModule', { value: true });

// ...なんやかんや
// some modules
exports.__esModule = true; // バグ踏む

パッチがあった

Cannot assign to read only property '__esModule' on Android 4.0 · Issue #2602 · facebook/create-react-app

そうだ、プラグイン作ろう

const ConcatSource = require("webpack-sources/lib/ConcatSource");
// バンドルしたファイルの冒頭にパッチをくっつけるだけ
const patch = `パッチの内容`;
module.exports = class DefinePropertyPatchPlugin {
  apply(compiler) {
    compiler.hooks.afterCompile.tap(
      "DefinePropertyPatchPlugin",
      compilation => {
        Object.keys(compilation.assets)
          .filter(file => /.js$/.test(file))
          .forEach(file => {
              const asset = compilation.assets[file];
              compilation.assets[file] = 
                  new ConcatSource(patch + asset.source());
          });
    });
  }
}

注意

雑に作ったのでくっつけた
パッチ部分が minify されない

AllowMutateEsmExportsPlugin
ES modules の仕様を裏切る…

1年前... webpack 2 -> 4 への移行時...

アップデートしたらテストが壊れた。

テストというか、テストダブルの箇所。

// mod.js
export function test() {
  return "fail";
}

// entry.js
import sinon from "sinon";
import * as mod from "./mod";

sinon.stub(mod, "test").returns("pass"); // ⬅
// `TypeError: Attempted to wrap undefined property as function`

そもそも ES modules って

仕様として exported module は read-only.
webpack が ES modules の仕様に厳格に。
export されたモジュールは書き換えできなくなった 🤔

webpack bootstrap コード

// ここを
Object.defineProperty(exports, name, {
  enumerable: true,
  get: getter
});
// こうしたい
Object.defineProperty(exports, name, {
  enumerable: true,
  configurable: true,
  get: getter
});

テストを回すバンドルの時だけ
無理やり書き換え。

module.exports = class AllowMutateEsmExportsPlugin {
  apply(compiler) {
    compiler.hooks.compilation.tap(
      "AllowMutateEsmExportsPlugin",
      compilation => {
      compilation.mainTemplate.hooks.requireExtensions.tap(
        "AllowMutateEsmExportsPlugin",
        source =>
        // 1つ前のスライド参照
        source.replace("ここを", "こうしたい")
      )
    });
  }
}

わかったこと: jest 使おう

以上です。
sample: tkdn/battle-saga-frontend-lt