Create React App 5.xでReactプロジェクトを作成するとmdx-js/loader 2.xが動作しない

React.jsとMDXを組み合わせて動かしたいな、という要件があり試したところ見事にハマった。

概要

Create React Appがバージョン5.xだとmdx-js/loader バージョン2.xが動作しない。MDXファイルが名前解決できずエラーとなってしまう。動作させるにはCRACOでCreate React AppのWebpack設定を上書きする必要がある。

再現手順

GitHub上には再現させたプロジェクトも存在する。自分で試した際も同じ状況になった。

github.com

自分が試したときのバージョンや関連ライブラリはこちら。

  • "@mdx-js/loader": "^2.2.1"
  • "@mdx-js/mdx": "^2.2.1"
  • "@types/mdx": "^2.0.3"
  • "react-scripts": "5.0.1"

Getting StartedのCreate React App (CRA)の通りに実施しても、次のようなエラーとなり動作しない。

// content.mdxを配置しているのに content.e417dbbe17c284a8b0c5.mdx になってしまう
Unhandled Thrown Error!
Failed to execute 'createElement' on 'Document': The tag name provided ('/static/media/content.e417dbbe17c284a8b0c5.mdx') is not a valid name.
Error: Failed to execute 'createElement' on 'Document': The tag name provided ('/static/media/content.e417dbbe17c284a8b0c5.mdx') is not a valid name.
    at createElement (http://localhost:3000/static/js/bundle.js:19147:38)
    at createInstance (http://localhost:3000/static/js/bundle.js:20136:24)
    at completeWork (http://localhost:3000/static/js/bundle.js:29756:32)
    at completeUnitOfWork (http://localhost:3000/static/js/bundle.js:33518:20)
    at performUnitOfWork (http://localhost:3000/static/js/bundle.js:33494:9)
    at workLoopSync (http://localhost:3000/static/js/bundle.js:33408:9)
    at renderRootSync (http://localhost:3000/static/js/bundle.js:33381:11)
    at recoverFromConcurrentError (http://localhost:3000/static/js/bundle.js:32873:24)
    at performConcurrentWorkOnRoot (http://localhost:3000/static/js/bundle.js:32785:26)
    at workLoop (http://localhost:3000/static/js/bundle.js:43088:38)
💿 Hey developer 👋

You can provide a way better UX than this when your app throws errors by providing your own errorElement props on <Route>
// content.mdx
# Hello, world!

This is **markdown** with <span style={{color: "red"}}>JSX</span>: MDX!
// App.tsx

import React from "react";
import "./App.css";
import Content from "./content.mdx";

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <p>This is React.js Project.</p>
        <nav>
          <ul>
            <li>
              <a href="react-page">to React.js Page</a>
            </li>
            <li>
              <a href="vue-page">to Vue.js Page</a>
            </li>
          </ul>
        </nav>
      </header>
      <article>
        <Content />
      </article>
    </div>
  );
}

export default App;

mdxファイルとApp.tsxは同じディレクトリに存在し、パス参照のエラーではないことは確認済み。

もっと詳しく

Create React App(以降CRAと表記)はReact.jsでシングルページアプリケーション(以降SPAと表記)をセットアップする際に使われる。 CRAの中にBabelやWebpackの設定が入っており、自分でセットアップせずにReact.jsのSPAが実行できる。バージョンアップへの追従も簡単に実行できる。1

問題はこの「CRAの中にBabelやWebpackの設定が入っており」の部分。MDXドキュメントのNoteを参照すると、次のように記載がある。

Note: rewiring with CRACO (see below) is currently required for CRA 5, due to a bug in react-scripts (facebook/create-react-app#12166), which is also tracked at mdx-js/mdx#1870.

mdx-js/mdx#1870に詳しい記載があるが、CRA側のWebpack設定に問題がある可能性が高い。

github.com

問題の箇所はasset/resourceの設定部分にある。

asset/resourceはWebpackの旧file-loaderのことで、アセットファイルをロードして使えるようにするためのモジュールである。

webpack.js.org

v4.webpack.js.org

filenameの設定が存在しない場合、ハッシュ値が付与された状態で出力先ディレクトリに配置される。問題はここにある。

MDXファイルはMarkdownの中にJSXを入れるので、ファイルではなくJSXとして扱ってもらいたい。しかし、CRAのWebpack設定を見るとasset/resourceのtestにMDX拡張子は存在しない。つまり、loaderを通すとハッシュ値ありのファイル扱いとなる。当然名前解決できず、エラーとなる。

解決するためには「mdx拡張子のファイルはasset/resourceの対象から除外する」設定が必要。修正パッチを投げている方がいるが、マージされる気配はない。

github.com

どうやって回避するの

2023/01/05時点では、CRAでプロジェクトを立ち上げてしまった場合CRA側のWebpack設定を無理やりカスタマイズするか、CRACOを用いるしかない。

無理やりカスタマイズするのであれば、create-react-app#12167のようにasset/resourceからmdx拡張子を除外する。

CARCOを使う場合、MDXドキュメントのCreate React App (CRA)に「Expand CRACO example」という項があり、そちらのサンプル通り設定してやると動作させることが可能。

しかし、CRACOはCRAの設定を上書きするためのライブラリなので、CRAの利点を潰してしまう。ドキュメントにも次のように記載がある。

CAUTION By doing this you're breaking the "guarantees" that CRA provides. That is to say, you now "own" the configs. No support will be provided. Proceed with caution.

よって、現時点でCRA v5.xとMDX v2.xを両立させるのは「できなくはないけどやめといた方が良い」と考えられる。

まとめ

  • Create React AppはWebpackやBabel設定が含まれている
  • asset/resourceはデフォルトでハッシュ値をつけてファイルをロードする
  • MDX側としてはJSX扱いの位置付けなのでasset/resourceの処理は不要
  • mdx-js/loaderが名前解決できずエラーとなる
  • 回避するならCreate React AppのWebpack設定を無理やり上書きするしかない

現状はReact.js + MDXではなくNext.js + MDXを使う / そもそもなんか別の方法で要件が満たせないのか考えた方が良さそうです。

参考


  1. 公式ドキュメント下部に記載されているが、npmのパッケージ名はreact-scriptsなので注意。