1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ReactRouterFramework v7 + MUI v6な環境で、ビルド時に発生する"Directory import ... is not supported resolving ES modules imported from..."エラーに対処する

Posted at

TL;DR

vite.config.tsssr.noExternal["@mui/*"] を追加する。
詳しくは こちら へ。

はじめに

SSR機能を無効化した React Router Framework v7 プロジェクトで Material UI v6 (MUI) を導入した際に発生したエラーについて、その原因と解決方法をメモしています。

問題の概要

react-router dev で開発サーバーは問題なく起動できるのに、 react-router build で下記エラーが発生する状況でした。

...
vite v5.4.11 building SSR bundle for production...
✓ 195 modules transformed.
build/server/.vite/manifest.json    0.17 kB
build/server/index.js             177.66 kB
x Build failed in 124ms
[react-router] Directory import '/path/to/project/sample-repo/node_modules/@mui/utils/formatMuiErrorMessage' is not supported resolving ES modules imported from /path/to/project/sample-repo/node_modules/@mui/material/styles/index.js
Did you mean to import "@mui/utils/formatMuiErrorMessage/index.js"?
    at finalizeResolution (node:internal/modules/esm/resolve:263:11)
    at moduleResolve (node:internal/modules/esm/resolve:932:10)
    at defaultResolve (node:internal/modules/esm/resolve:1056:11)
    at ModuleLoader.defaultResolve (node:internal/modules/esm/loader:654:12)
    at ModuleLoader.#cachedDefaultResolve (node:internal/modules/esm/loader:603:25)
    at ModuleLoader.resolve (node:internal/modules/esm/loader:586:38)
    at ModuleLoader.getModuleJobForImport (node:internal/modules/esm/loader:242:38)
    at ModuleJob._link (node:internal/modules/esm/module_job:135:49) {
  code: 'PLUGIN_ERROR',
  url: 'file:///path/to/project/sample-repo/node_modules/@mui/utils/formatMuiErrorMessage',
  pluginCode: 'ERR_UNSUPPORTED_DIR_IMPORT',
  plugin: 'react-router',
  hook: 'writeBundle'
}

SSRを有効化している場合のエラー内容

ちなみにSSRを有効化している場合は、ビルドが成功する代わりに開発サーバーの起動に失敗する状態でした。

Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object.

環境

$ npx envinfo --system --binaries \
  --npmPackages '{vite,react*,@react-router/*,@mui/*,@emotion/*,typescript}'

  System:
    OS: macOS 15.1.1
    CPU: (8) arm64 Apple M2
    Memory: 104.55 MB / 8.00 GB
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 22.12.0 - ~/.volta/tools/image/node/22.12.0/bin/node
    npm: 10.9.0 - ~/.volta/tools/image/node/22.12.0/bin/npm
  npmPackages:
    @emotion/react: ^11.14.0 => 11.14.0 
    @emotion/styled: ^11.14.0 => 11.14.0 
    @mui/material: ^6.3.1 => 6.3.1 
    @react-router/dev: ^7.1.1 => 7.1.1 
    @react-router/node: ^7.1.1 => 7.1.1 
    @react-router/serve: ^7.1.1 => 7.1.1 
    react: ^19.0.0 => 19.0.0 
    react-dom: ^19.0.0 => 19.0.0 
    react-router: ^7.1.1 => 7.1.1 
    typescript: ^5.7.2 => 5.7.2 
    vite: ^5.4.11 => 5.4.11

対処法

vite.config.tsssr.noExternal["@mui/*"] を追加します。

 // vite.config.ts
 export default defineConfig({
   plugins: [reactRouter(), tsconfigPaths()],
   server: {
     open: true,
   },
+  ssr: {
+    noExternal: ["@mui/*"],
+  },
 });

この設定により、ViteはMUI関連のモジュールを外部モジュールとして扱わず、プロジェクト内でバンドルするようになります。

少しでもHMRを高速化させたいなど、wildcardを使いたくない場合は、以下のように @mui 配下のpackage-nameを個別指定できます。

  ssr: {
    noExternal: [
      "@mui/material",
      "@mui/utils",
      "@mui/styled-engine",
      "@mui/system",
    ],
  }

エラーは解決しましたが、せっかくなので対処法について少し深掘りしてみましょう。
まずはエラー原因について推察し、 ssr.noExternal の指定による挙動の変化を見てみます。

エラーが起きていた原因

筆者はESMやCJSといったJavaScript モジュールに詳しいわけではありません。
内容に誤解が含まれる可能性がある点をご承知おきください。

エラーメッセージをもう一度確認してみましょう。
not supported resolving ES modules imported
このメッセージから、どうやらESM形式のimport文の仕様を満たせていない可能性が高いと考えられます。

エラーが発生している node_modules/@mui/material/styles/index.js (@mui/material@6.3.1) ファイルを確認すると以下のようなコードが含まれています。

// node_modules/@mui/material/styles/index.js を一部抜粋
import _formatMuiErrorMessage from "@mui/utils/formatMuiErrorMessage";
export { default as THEME_ID } from "./identifier.js";
...

"@mui/utils/formatMuiErrorMessage" の部分で拡張子が省略されています。しかし、ESMの仕様では import 文で拡張子を省略しないことが求められています。
(TypeScriptでimport文を書く時は拡張子を省略するのが一般的ですが、TypeScriptコンパイラやバンドラがtsconfigなどの設定に沿ってよしなにやってくれてる認識です。これ以上は難しくて分かりません。)

モジュールを含む .js ファイルへの相対または絶対 URL となっています。Node では、拡張子なしのインポートは node_modules におけるパッケージへの参照であることが多いです。バンドラーによっては、拡張子を省略してもよいことにしています。環境を確認してください。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/import#構文 より引用

エラーメッセージには次のような修正案が記載されています。
Did you mean to import "@mui/utils/formatMuiErrorMessage/index.js"?

つまり、拡張子付きで指定すれば問題は発生しないようです。しかし、このコードはMUIライブラリ内のものであり、開発者が直接修正することはできません。そのため、ssr.noExternal オプションを使用し、Viteによるバンドル設定を変更することで対処しました。

ssr.noExternal の指定で何が行われているのか

以下はVite公式ドキュメントからの引用です。

(補足: ssr.noExternal を使用することで)指定した依存関係が SSR のために外部化されるのを防ぎます。

https://ja.vite.dev/config/ssr-options#ssr-noexternal より引用

SSR を実行する場合、依存関係はデフォルトで Vite の SSR トランスフォームモジュールシステムから「外部化」されます。これにより、開発とビルドの両方を高速化します。

https://ja.vite.dev/guide/ssr#ssr-externals より引用

ここで「外部化」とは、依存関係をViteのSSRモジュールトランスフォームシステム外で処理することを指しています。
ssr.noExternal にMUIモジュールを指定すると、Viteがそのモジュールをトランスパイルおよびバンドルするようになるということだと認識しています。

例として、 ssr.noExternal["@mui/*"] を指定した場合のビルドアセットを確認してみます。

spaモードのままだとビルド完了後にサーバー向けassetは削除されてしまうので、確認のために一旦ssrを有効化します。
// react-router.config.ts
import type { Config } from "@react-router/dev/config";

export default {
- ssr: false,
+ ssr: true,
} satisfies Config;

ビルド時にエラーが発生していたサーバー用アセット ( build/server/index.js ) を確認すると、 @mui/utils/formatMuiErrorMessage は外部モジュールからimportするのではなくローカル関数として記述されていることがわかります。
これは、ViteがMUIモジュールを1つのJavaScriptファイルにバンドルした結果です。

build/server/index.js
...
function formatMuiErrorMessage(code, ...args) {
  const url = new URL(`https://mui.com/production-error/?code=${code}`);
  args.forEach((arg2) => url.searchParams.append("args[]", arg2));
  return `Minified MUI error #${code}; visit ${url} for the full message.`;
}
...

この動作は、以下の公式ドキュメントで説明されています。

webworker のランタイムなどの場合、SSR のビルドを 1 つの JavaScript ファイルにバンドルしたい場合があります。ssr.noExternal を true に設定することで、この動作を有効にできます。

https://ja.vite.dev/guide/ssr#ssr-バンドル より引用

最後に

JavaScriptモジュール(ESM/CJS)の仕組みは非常に奥が深く、簡単に理解できるものではありませんね。
なお、MUIの次のメジャーバージョン(v7)ではESMの完全サポートが予定されています。この変更により、今回のような問題が解決されることが期待されます。

Material UI v7 (draft) Milestone

参照資料

@mui/icons-materialを使うとCannot use import statement outside a moduleと言われる件

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?