🤖

`SyntaxError: Named export 'renderToReadableStream' not found.`を解決する

2025/01/04に公開

要約

対象

  • Node.js環境でreact-dom/serverrenderToReadableStreamを使用する場合
  • (より具体的には)React Router v7 + Cloudflareでプリレンダリングやビルドファイルを用いたスクリプトを実行する場合

解決法

ライブラリからインポートを行うとき、指定した条件キーを用いて解決するようNode.jsに命令します。
--conditions <key>または-C <key>のようにして指定することができます。

node --conditions workerd ./server.js
// or
NODE_OPTIONS='-C workerd' npm run your-script

Windowsユーザーの場合、上記コマンドを実行すると停止するためcross-envを用いてnpm scriptsとして実行します。

npm i cross-env
{
  "scripts": {
    "dev": "cross-env NODE_OPTIONS=\"-C workerd\" npm run your-script"
  }
}

ちなみにworkerdは、react-dom@19かつCloudflareを用いる場合の条件キーです。react-dom@18およびBunやDenoなどを用いる場合は次の表のように読み替えてください。

ランタイム react-dom@19 react-dom@18
Cloudflare workerd browser
Bun bun browser
Deno deno deno
その他 browserまたはedge-lightなど browser

問題の所在

サーバー上でReact コンポーネントをHTMLにレンダーしたい場合、renderToReadableStreamを用いることがあります。
このコードをNode.js環境で実行[1]しようとすると次のようなエラーが発生します。

import { renderToReadableStream } from 'react-dom/server'

SyntaxError: Named export 'renderToReadableStream' not found. The requested module 'react-dom/server' is a CommonJS module, which may not support all module.exports as named exports.

この問題に関連するものとして以下を参照してください。

要約すると、react-dom/serverからインポートを行うときエッジ環境とNode.js環境で読み込まれるファイルが異なります。そしてNode.js環境で実行するファイル内ではrenderToReadableStreamが定義されていないため上記エラーが発生します。

Node.jsの--conditionsを変更する

指定条件に応じて異なる実行ファイルを提供するという機能は、Node.jsのConditional Exportsに関するものです。

具体的な動作を確認してみましょう。
react-dom@19package.jsonから一部省略して引用)

{
  "exports": {
    "./server": {
      "workerd": "./server.edge.js",
      "node": "./server.node.js"
    },
  }
}
  1. Node.jsではデフォルトの条件として"node", "default", "import", "require"が常に指定され解決対象となります(ソース)。
  2. react-dom/serverからのインポートを試みるとき、package.jsonで指定された"node"キーのマッピングに対応した./server.node.jsからファイルが読み込まれますが、./server.node.jsではrenderToReadableStreamが定義されていないためエラーが発生します。

ここでNode.jsの--conditonsを用いて追加指定し、解決対象を変更すれば実行ファイルを変えることができます。

コマンドラインオプションから変更する場合は、--conditionsまたは-Cを用います。(ソース

node -C workerd ./server.js

また環境変数から変更することも可能です。

NODE_OPTIONS='-C workerd' npm run your-script

Windowsユーザーの場合、上記コマンドを実行すると停止するためcross-envを用いてnpm scriptsとして実行します。

npm i cross-env
{
  "scripts": {
    "dev": "cross-env NODE_OPTIONS=\"-C workerd\" npm run your-script"
  }
}

なお条件は複数指定できます。react-domの場合細かく条件分けされているのですが、ライブラリによっては単にbrowserdefaultのみ提供されている可能性もあるのでworkerd, worker, browserと複数指定しておくとよいでしょう。

node -C workerd -C worker -C browser" ./server.js
// or
NODE_OPTIONS='-C workerd -C worker -C browser' npm run your-script 

条件の優先順位

ここで疑問が生じます。workerdを明示的に指定するとき常にnodeに優先するのでしょうか。あるいは条件を複数指定する場合、どの値が優先して読み込まれるのでしょうか。

Within the "exports" object, key order is significant. During condition matching, earlier entries have higher priority and take precedence over later entries. The general rule is that conditions should be from most specific to least specific in object order. - ソース

つまり、exportsオブジェクトのなかで指定されたキーの順番に応じて、読み込みファイルを決定するということです。--conditions, -Cで明示的に指定したことや指定した順序は読み込みファイルの優先順位に関係ありません。

この点について簡単なリポジトリを用意しています。npm run devを実行する際に--conditionsを変更すると、ログに出力される環境も変更されます。具体的には以下の動作です。

  • -C workerdに設定するとThis is worked environment!と出力されます。
  • -C nodeに設定するとThis is node environment!と出力されます。
  • -C browserに設定するとThis is node environment!と出力されます。これはライブラリのexportオブジェクトにおいてnodeのほうがbrowserより優先順位が高いからです。

別の方法

インポートパスを直接指定する

さて既に参照した記事にあるようにインポートパスを直接指定することでも動作します。

import { renderToReadableStream } from 'react-dom/server.browser' // 'react-dom/server'ではない!

しかし、この方法についてバグが発生する可能性について言及したものもあります。またライブラリによってはエクスポートのキーマッピングが変更されアプリが壊れる可能性も否定できません[2]

一方で条件を指定するというこの投稿の方法が常に万能というわけではありません。前述のようにexportsオブジェクトのなかで指定されたキーの順番に応じて、読み込みファイルを決定されます。仮にライブラリがnodeworkerdより上位に指定している場合、条件にworkerdを指定したところでnodeが読み込まれてしまうということを意味します[3]。必要に応じて使い分けましょう。

Viteを用いている場合

Viteを用いた開発については、ssr.resolve.conditionsssr.resolve.externalConditionsを用いてください。

export default defineConfig({
  ssr: {
    resolve: {
      conditions: [
          'workerd',
          'worker',
          'browser',
        ],
      externalConditions: ['workerd', 'worker']
    },
  },
})

React Router v7 + CloudflareでPre-renderingするとエラーが出る問題

React Router v7(投稿時点でv7.1.1) + Cloudflareにおいて(Node.js環境で)開発できるのにPre-renderingを使用するとエラーが発生する問題があります。

コードを軽く流し読みした程度で間違えている可能性もあるのですおそらく次のような動作をしています。開発自体はViteを用いて動作します。一方でPre-renderingは、Viteを用いるのではなく、Node.js上でビルドファイルを読み込みリクエストをシミュレーションすることで行われます。そこでViteの設定とは別にNode.jsの条件指定をする必要があります。

buildコマンドを次のように変更してください。

{
  "scripts": {
-   "build": "react-router build",
+   "build": "cross-env NODE_OPTIONS=\"--conditions workerd\" react-router build",  
  },
  "devDependencies": {
+   "cross-env": "^7"
  }
}

RemixのCloudflare workersテンプレートをReact Router v7にアップグレードしてPre-renderingが動作するように調整したこちらのリポジトリおよびコミットも参照してください。

その他参照

脚注
  1. 一般にNode.js環境ではrenderToPipeableStreamを使うことになりますが、Web標準上にサーバーを実装したいということもあると思います。現にHonoをNode.jsで動かす向きもあります。また、より一般的な事例としてはエッジ環境向けにrenderToReadableStreamを用いて作成したファイルを、様々な処理のためにNode.js環境で読みこむことを想定しています。 ↩︎

  2. 実をいうと、この記事を作成した理由はインポートのpathを直接指定する方法を用いていたところ、React@18からReact@19へのアップデートでexportオブジェクトに変更がありモジュールを読み込めなくなったという問題に起因します。 ↩︎

  3. この点についてStandardize condition order so that edge-lite preferred over browser #29877を参照してください。 ↩︎

Discussion