TL;DR
vite.config.ts
の ssr.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.ts
の ssr.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 のために外部化されるのを防ぎます。
SSR を実行する場合、依存関係はデフォルトで Vite の SSR トランスフォームモジュールシステムから「外部化」されます。これにより、開発とビルドの両方を高速化します。
ここで「外部化」とは、依存関係を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ファイルにバンドルした結果です。
...
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 に設定することで、この動作を有効にできます。
最後に
JavaScriptモジュール(ESM/CJS)の仕組みは非常に奥が深く、簡単に理解できるものではありませんね。
なお、MUIの次のメジャーバージョン(v7)ではESMの完全サポートが予定されています。この変更により、今回のような問題が解決されることが期待されます。
参照資料
@mui/icons-materialを使うとCannot use import statement outside a moduleと言われる件