@babel/core@7.13.7とnode-semver@7.0.0とwebpackを併用してハマった話

TL;DR

@babel/core@7.13.7以下をwebpackと一緒に使うとnode-semver@7.0.0でハマるかもしれない

はじめに

仕事でブラウザ上のエディタでmarkdownを書くとリアルタイムにReactでどんなコンテンツが表示されるかを確認できるコンテンツシミュレーターみたいなものを使っている。

このシミュレーターで書けるmarkdownは本来のmarkdownをちょっと拡張して、独自にJS書けたり、インラインでJS関数やオブジェクトの参照ができたりする。

このシミュレーターが既存のPJでは動いているのに、別のPJで新しくセットアップしようとしたら動かないという事象を調査したら、意外なところまで行ったのでその経緯と顛末を書く

発端

あるPJ(以下A PJ)でこんな感じのmdを書くとシミュレーターのコンパイルが失敗するという事象で困ってると相談を受けた。

こんにちは、${context.name}さん。

->

JavaScriptの構文エラーが発生しました
...(Trace的なやつ)

本当はこうなってほしい

こんにちは、${context.name}さん。

->

こんにちは、田中さん

コンテンツを以下のようにすると、特に問題なくシミュレーターが動くので、どうも${context.name}のところのJSの評価がうまく動かないらしい

こんにちは、世界

なぜエラーになるのか

このシミュレーターの仕組みは、エディタ上で与えられたmdコンテンツをworkerでコンパイルして、JSを評価して表示するコンテンツを生成する。

コンパイルでmarkedとbabelを使っている。

表示するためにReactのコンポーネントを書いておくという感じ。

とりあえずTraceが出てるので、宇宙より壮大なnode_modules配下のbabelされたりしたJSたちにひたすら潜ってlogを挿したりしながら、エラーメッセージをたどっていく。

これが宇宙の旅?

すると最終的に以下に辿り着いた

transform error Error: [BABEL] /[anonymous]: Cannot find module './functions/satisfies' (While processing: "base$0")
    at webpackEmptyContext (eval at ../../node_modules/@babel/core/node_modules/semver sync recursive (82363a058d1f41f6955e.worker.js:1872), <anonymous>:2:10)
    at lazyRequire (index.js?0488:5)
    at Object.get [as satisfies] (index.js?0488:12)
    at Object.assertVersion (config-api.js?bf03:91)
    at eval (index.js?8294:252)
    ...

なんだか、@babel/coreが抱えているsemverでmoduleが読み込めないらしい

node_modulesの宇宙からsemverのpackage.jsonを見つけ出してmainのindex.jsを読んでみる

const lrCache = {}
const lazyRequire = (path, subkey) => {
  const module = lrCache[path] || (lrCache[path] = require(path))
  return subkey ? module[subkey] : module
}

const lazyExport = (key, path, subkey) => {
  Object.defineProperty(exports, key, {
    get: () => {
      const res = lazyRequire(path, subkey)
      Object.defineProperty(exports, key, {
        value: res,
        enumerable: true,
        configurable: true
      })
      return res
    },
    configurable: true,
    enumerable: true
  })
}

lazyExport('re', './internal/re', 're')
...(略)
lazyExport('satisfies', './functions/satisfies')
...(略)

lazyyRequireの機構を自前で実装しているらしい。

logを差し込んで試すと、const module = lrCache[path] || (lrCache[path] = require(path))がORの右辺に落ちてrequire(path)でmoduleが見つからずにエラーになっているっぽい。

bundle時に静的に決まっていないrequireをbundlerで解決できないのはそれはそうという感じ(webpackの挙動に詳しくないので違うかもしれない)

なぜ動いているのか

エラーの原因は分かったが、今度はなぜ以前設定したPJ(以下B PJ)では今でも動いているのか謎なので同様に調査

semberのpackage.jsonのmainにあるsemver.jsがこちら

exports = module.exports = SemVer

var debug
/* istanbul ignore next */
if (typeof process === 'object' &&
    process.env &&
    process.env.NODE_DEBUG &&
    /\bsemver\b/i.test(process.env.NODE_DEBUG)) {
  debug = function () {
    var args = Array.prototype.slice.call(arguments, 0)
    args.unshift('SEMVER')
    console.log.apply(console, args)
  }
} else {
  debug = function () {}
}
...(略)

なるほど、全然違う

というかmainに書いてある値が違うのでsemverのバージョン違いが原因らしい

そのsemverはどこからやってきたのか

エラーになるA PJのsemverのversionは7.0.0

これは、@babel/core7.13.1から指定されている

エラーにならないB PJのsemverのversionは5.7.1

これは、@babel/core7.12.3から指定されている

@babel/coreが上がってるのは気づいていたが、minor versionだから大丈夫やろと油断していた

いつ上がったのか@babel/coreのリリースブログchangelogを読んでみるが、それっぽい記述がない

blameするとこいつで変わったらしい

github.com

BABEL_8でbreakingするよ的なflagが立っていて、@babel/coreのv7.13系からsemverがv7系に上がっている

これがPR

github.com

PRマージされた後に、@babel/coreとwebpackを併用するとsemverが7.0.0に固定されてissueが起こると書いてある

このコメント、ブラウザでコードエディタやってて@babel/core使ってると書いてあるので、まったく同じユースケースでびっくり

そのあとsemver@v7.1系を使うとBREAKINGなので、6.3系にダウングレードするPR出されて、まだリリースされていない。これが2日前。

github.com

ということで、次のbabelがリリースされたら解決しそう

おまけ1: semverのpreload機構

2019年12月ぐらいからちょこちょこissueにはなっていたらしい github.com

ドキュメントが更新されたりしていたが、以下のPRで廃止になっている

github.com

おまけ2: semverのsemver

github.com

node-semverのレポジトリを調査していたら、node-semverがsemverに従ってないというコメントがついていて、メンテナからするとそんなことはないよなあと思ったので貼っておく

おわりに

ということで、@babelのv7.13系の踏むのが難しいバグを踏み抜いたお話でした。

@babelのv7.13.0は2021-02-22にリリースされて、まだ4日しか経っていないのにもうv7.13.7なので、すぐにfixされるでしょう。

@babelもnode-semverももともとはnode.jsで使うことを想定されていて、ブラウザ環境で使われるのはエコシステムが発展したが故なので、この辺にハマるときはきっちりハマります。

メンテナはユースケースが複雑なので非常に大変だと思いますが、メンテしてもらっている身だとありがたい限りです。自分もブログを書いて他にハマる人がいたときの一助になればよいなと思いつつ、おしまい。