diff --git a/.gitignore b/.gitignore
index 3b4b50be..9491e704 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,6 +15,10 @@ netlify.toml
deno.lock
.azure
.azure-swa
+.firebase-app
+.firebase
+.firebaserc
+firebase.json
.bun
.deno
.aws
diff --git a/docs/react-server.wrangler.toml b/docs/react-server.wrangler.toml
index e4bdf1f7..270b427b 100644
--- a/docs/react-server.wrangler.toml
+++ b/docs/react-server.wrangler.toml
@@ -1,3 +1,3 @@
[[routes]]
-pattern = "react-server.dev/*"
-zone_name = "react-server.dev"
\ No newline at end of file
+pattern = "react-server.dev"
+custom_domain = true
\ No newline at end of file
diff --git a/docs/src/components/AdapterGrid.jsx b/docs/src/components/AdapterGrid.jsx
index 6c4f5e69..1a9f3529 100644
--- a/docs/src/components/AdapterGrid.jsx
+++ b/docs/src/components/AdapterGrid.jsx
@@ -1,53 +1,90 @@
+import { useLanguage } from "../i18n.mjs";
+import { defaultLanguage } from "../const.mjs";
+
const adapters = [
{
name: "Vercel",
href: "/deploy/vercel",
- description: "Serverless & edge functions",
+ description: {
+ en: "Serverless & edge functions",
+ ja: "サーバーレス & エッジ関数",
+ },
},
{
name: "Netlify",
href: "/deploy/netlify",
- description: "Serverless functions & edge CDN",
+ description: {
+ en: "Serverless functions & edge CDN",
+ ja: "サーバーレス関数 & エッジ CDN",
+ },
},
{
name: "Cloudflare",
href: "/deploy/cloudflare",
- description: "Workers & Pages",
+ description: {
+ en: "Workers & Pages",
+ ja: "Workers & Pages",
+ },
},
{
name: "AWS Lambda",
href: "/deploy/aws",
- description: "Serverless functions",
+ description: {
+ en: "Serverless functions",
+ ja: "サーバーレス関数",
+ },
},
{
name: "Bun",
href: "/deploy/bun",
- description: "Standalone Bun server",
+ description: {
+ en: "Standalone Bun server",
+ ja: "スタンドアロン Bun サーバー",
+ },
},
{
name: "Deno",
href: "/deploy/deno",
- description: "Standalone Deno server",
+ description: {
+ en: "Standalone Deno server",
+ ja: "スタンドアロン Deno サーバー",
+ },
},
{
name: "Azure Functions",
href: "/deploy/azure",
- description: "Functions v4 with streaming",
+ description: {
+ en: "Functions v4 with streaming",
+ ja: "ストリーミング対応 Functions v4",
+ },
},
{
name: "Azure Static Web Apps",
href: "/deploy/azure-swa",
- description: "Managed functions & CDN",
+ description: {
+ en: "Managed functions & CDN",
+ ja: "マネージド関数 & CDN",
+ },
+ },
+ {
+ name: "Firebase Functions",
+ href: "/deploy/firebase",
+ description: {
+ en: "Cloud Functions v2 with streaming",
+ ja: "ストリーミング対応 Cloud Functions v2",
+ },
},
];
export default function AdapterGrid() {
+ const lang = useLanguage();
return (
-
+
{adapters.map(({ name, href, description }) => (
@@ -55,7 +92,7 @@ export default function AdapterGrid() {
{name}
- {description}
+ {description[lang] ?? description.en}
))}
diff --git a/docs/src/pages/en/(pages)/deploy/adapters.mdx b/docs/src/pages/en/(pages)/deploy/adapters.mdx
index 204a9c2b..d0fcfae6 100644
--- a/docs/src/pages/en/(pages)/deploy/adapters.mdx
+++ b/docs/src/pages/en/(pages)/deploy/adapters.mdx
@@ -4,6 +4,7 @@ category: Deploy
order: 0
---
+import AdapterGrid from "../../../../components/AdapterGrid.jsx";
import Link from "../../../../components/Link.jsx";
# Adapters
@@ -14,25 +15,16 @@ You can use adapters to configure your app for different deployment environments
### Available adapters
-- [x] Vercel
-- [x] Netlify
-- [x] Cloudflare Workers/Pages
-- [x] AWS Lambda
-- [x] Bun
-- [x] Deno
-- [x] Azure Functions
-- [x] Azure Static Web Apps
+
## Configuration
-Add `adapter` entry to your `react-server.config.mjs` file. You can specify the name of a built-in adapter (`vercel`, `netlify`, `cloudflare`, `aws`, `bun`, `deno`, `azure`, or `azure-swa`) as a string, or use an external adapter package.
+Add `adapter` entry to your `react-server.config.mjs` file. You can specify the name of a built-in adapter (`vercel`, `netlify`, `cloudflare`, `aws`, `bun`, `deno`, `azure`, `azure-swa`, or `firebase`) as a string, or use an external adapter package.
> **Note:** When running a production build with **Bun** or **Deno**, the corresponding adapter is automatically detected and used without any configuration. You can override this with an explicit `adapter` setting in your config or via `--adapter
` on the CLI. Use `--no-adapter` to disable auto-detection.
-> **Note:** The `aws` adapter requires the [AWS SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html) and configured AWS credentials. See the [AWS Lambda](/deploy/aws) page for setup instructions.
-
```mjs
export default {
adapter: 'vercel',
diff --git a/docs/src/pages/en/(pages)/deploy/api.mdx b/docs/src/pages/en/(pages)/deploy/api.mdx
index 20785a8e..19445909 100644
--- a/docs/src/pages/en/(pages)/deploy/api.mdx
+++ b/docs/src/pages/en/(pages)/deploy/api.mdx
@@ -57,7 +57,7 @@ You need to pass adapter properties to the `createAdapter` function to configure
`handler`: The adapter handler function.
-`deploy`: The deployment command and arguments. This is optional. When provided, the adapter will show what command the developer needs to run to deploy the application after it has been built. If the `--deploy` flag is provided during the build, the adapter will run this command. The `deploy` property can also be a function that will be called with the adapter options, CLI options and the handler result. This is useful if you need to customize the deployment command based on the adapter options or the handler result. If you don't provide a result with `command` and `args`, the default deployment handling spawning the command will be skipped. This is useful if you want to implement a custom deployment workflow in the adapter.
+`deploy`: The deployment command and arguments. This is optional. When provided, the adapter will show what command the developer needs to run to deploy the application after it has been built. If the `--deploy` flag is provided during the build, the adapter will run this command. You can also pass a JSON string to `--deploy` (e.g. `--deploy '{"project": "my-project"}'`) to provide additional adapter options at deploy time — these are merged into `adapterOptions` with CLI values taking precedence. The `deploy` property can also be a function that will be called with the adapter options, CLI options and the handler result. This is useful if you need to customize the deployment command based on the adapter options or the handler result. If you don't provide a result with `command` and `args`, the default deployment handling spawning the command will be skipped. This is useful if you want to implement a custom deployment workflow in the adapter.
The deploy descriptor supports the following properties:
diff --git a/docs/src/pages/en/(pages)/deploy/firebase.mdx b/docs/src/pages/en/(pages)/deploy/firebase.mdx
new file mode 100644
index 00000000..d5752476
--- /dev/null
+++ b/docs/src/pages/en/(pages)/deploy/firebase.mdx
@@ -0,0 +1,197 @@
+---
+title: Firebase
+category: Deploy
+order: 8
+---
+
+import Link from "../../../../components/Link.jsx";
+
+# Firebase Functions
+
+To deploy to Firebase, use the built-in `firebase` adapter. This adapter uses Firebase Cloud Functions (v2) with Firebase Hosting rewrites, bundling your server into a single edge file with full response streaming support.
+
+
+## Installation
+
+
+You need the [Firebase CLI](https://firebase.google.com/docs/cli) installed:
+
+```sh
+npm install -g firebase-tools
+
+# Login to Firebase
+firebase login
+```
+
+No additional packages are needed — the adapter is built into `@lazarv/react-server`.
+
+Your Firebase project must be on the **Blaze (pay-as-you-go) plan** to deploy Cloud Functions. You can upgrade in the [Firebase Console](https://console.firebase.google.com).
+
+Add the adapter to your `react-server.config.mjs` file:
+
+```mjs
+export default {
+ adapter: "firebase",
+};
+```
+
+
+## Configuration
+
+
+You can customize the adapter by passing options:
+
+```mjs
+export default {
+ adapter: [
+ "firebase",
+ {
+ project: "my-project", // Firebase project ID
+ region: "us-central1", // Function region
+ memory: "512MiB", // Memory allocation
+ timeoutSeconds: 60, // Request timeout
+ minInstances: 0, // Minimum warm instances
+ maxInstances: 100, // Maximum concurrent instances
+ concurrency: 80, // Requests per instance
+ hosting: {}, // Extra firebase.json hosting properties
+ functions: {}, // Extra firebase.json functions properties
+ firebase: {}, // Extra top-level firebase.json properties
+ },
+ ],
+};
+```
+
+### Configuration Options
+
+- `project`: Firebase project ID. Falls back to `package.json` name (without scope prefix).
+- `region`: Cloud Functions region (default: `"us-central1"`). See [available regions](https://cloud.google.com/functions/docs/locations).
+- `memory`: Memory allocation for the function (default: `"512MiB"`). Valid values: `"128MiB"`, `"256MiB"`, `"512MiB"`, `"1GiB"`, `"2GiB"`, `"4GiB"`, `"8GiB"`, `"16GiB"`, `"32GiB"`.
+- `timeoutSeconds`: Maximum request duration in seconds (default: `60`). Maximum is 540 for v2 functions.
+- `minInstances`: Minimum number of warm instances to keep ready (default: `0`). Setting this above 0 reduces cold starts but incurs costs.
+- `maxInstances`: Maximum number of concurrent instances (default: `100`).
+- `concurrency`: Maximum concurrent requests per instance (default: `80`).
+- `hosting`: Additional properties to merge into the `hosting` section of `firebase.json`.
+- `functions`: Additional properties to merge into the `functions` section of `firebase.json`.
+- `firebase`: Additional top-level properties to merge into `firebase.json`.
+
+You can also pass adapter options at deploy time via the `--deploy` flag:
+
+```sh
+pnpm react-server build --adapter firebase --deploy='{"project":"my-project"}'
+```
+
+
+## Deploy
+
+
+Build and deploy in one step:
+
+```sh
+pnpm react-server build --deploy
+```
+
+Or build first and deploy manually:
+
+```sh
+# Build
+pnpm react-server build
+
+# Deploy using Firebase CLI
+firebase deploy --only functions,hosting --project
+```
+
+
+## Response Streaming
+
+
+This adapter supports response streaming. React Server Components, Suspense boundaries, and progressive HTML delivery all work out of the box. The generated Cloud Function streams the response body chunk by chunk using the Node.js response object's `write()` method.
+
+
+## How it works
+
+
+The adapter uses an **edge build** mode, bundling your entire server into a single file. At build time, it:
+
+1. Bundles your server into `.firebase-app/server/.react-server/server/edge.mjs`
+2. Copies all static assets into `.firebase-app/public/`
+3. Generates a `src/index.mjs` wrapper that:
+ - Imports `onRequest` from `firebase-functions/v2/https`
+ - Registers an HTTP-triggered function matching all methods and paths
+ - Serves static files from a build-time route map using `readFileSync()`
+ - Converts incoming Express-style requests to standard `Request` objects
+ - Delegates dynamic requests to the bundled edge handler
+ - Streams the response back
+4. Generates `package.json` with `firebase-functions` and `firebase-admin` dependencies
+5. Runs `npm install` in `.firebase-app/` to install dependencies
+6. Generates `firebase.json` with Hosting rewrites pointing to the function
+7. Generates `.firebaserc` with the project ID (if available)
+
+> **Important:** The `firebase-functions` and `firebase-admin` packages cannot be bundled into the edge build. The Firebase Functions runtime expects these as external dependencies in `node_modules`.
+
+
+## Static Files
+
+
+Static files are served by Firebase Hosting's CDN when possible. All static assets (HTML, CSS, JS, images) are copied to `.firebase-app/public/` and served directly by Hosting. The function also has a static file route map as a fallback for any requests that reach the function.
+
+Assets under `/assets/` and `/client/` paths are served with immutable cache headers (`Cache-Control: public, max-age=31536000, immutable`).
+
+
+## Local Development
+
+
+After building, you can test locally using the Firebase Emulator:
+
+```sh
+firebase emulators:start --only functions,hosting
+```
+
+
+## Output Structure
+
+
+```
+.firebase-app/
+├── package.json # Dependencies (firebase-functions, firebase-admin)
+├── node_modules/ # Installed dependencies
+├── public/ # Static assets (HTML, CSS, JS, images)
+├── server/
+│ └── .react-server/ # Bundled server (edge.mjs, manifests)
+└── src/
+ └── index.mjs # Firebase Cloud Function entry
+
+firebase.json # Firebase configuration (Hosting + Functions)
+.firebaserc # Firebase project alias
+```
+
+
+## Troubleshooting
+
+
+### "Blaze plan required" error
+
+Firebase Cloud Functions require the Blaze (pay-as-you-go) billing plan. Upgrade your project at the [Firebase Console](https://console.firebase.google.com). The Blaze plan still includes generous free-tier usage — you only pay for actual usage beyond the free limits. The billing plan is per-project and does not affect your other Firebase projects.
+
+### Cold starts
+
+By default, `minInstances` is `0`, meaning the function scales to zero when idle. The first request after scaling to zero will experience a cold start (typically 1-3 seconds). Set `minInstances: 1` to keep one instance warm:
+
+```mjs
+export default {
+ adapter: ["firebase", { minInstances: 1 }],
+};
+```
+
+### "Could not find project" error
+
+Make sure your project ID is correct. You can list your Firebase projects with:
+
+```sh
+firebase projects:list
+```
+
+Set the project explicitly via adapter options or the `--deploy` flag:
+
+```sh
+pnpm react-server build --adapter firebase --deploy='{"project":"my-project"}'
+```
diff --git a/docs/src/pages/en/(pages)/framework/cli.mdx b/docs/src/pages/en/(pages)/framework/cli.mdx
index 328561f7..d4ef896a 100644
--- a/docs/src/pages/en/(pages)/framework/cli.mdx
+++ b/docs/src/pages/en/(pages)/framework/cli.mdx
@@ -203,6 +203,14 @@ Deploy using adapter. Default is `false`.
If you use an adapter in your `react-server.config.mjs` file, the adapter will pre-build your app for deployment and when you use this argument, the adapter will also deploy your app at the end of the build process.
+You can also pass a JSON string to `--deploy` to provide adapter-specific options at deploy time. These options are merged with the `adapterOptions` from your configuration file, with the CLI values taking precedence.
+
+```sh
+pnpm react-server build --deploy '{"project": "my-project"}'
+```
+
+This is useful when you want to override or provide adapter options without modifying your configuration file, for example specifying a deployment target project or environment.
+
### --eval
diff --git a/docs/src/pages/ja/(pages)/deploy/adapters.mdx b/docs/src/pages/ja/(pages)/deploy/adapters.mdx
index 87fc6041..f46baf3a 100644
--- a/docs/src/pages/ja/(pages)/deploy/adapters.mdx
+++ b/docs/src/pages/ja/(pages)/deploy/adapters.mdx
@@ -5,6 +5,7 @@ order: 0
---
import Link from "../../../../components/Link.jsx";
+import AdapterGrid from "../../../../components/AdapterGrid.jsx";
# アダプタ
@@ -14,25 +15,16 @@ import Link from "../../../../components/Link.jsx";
### 利用可能なアダプタ
-- [x] Vercel
-- [x] Netlify
-- [x] Cloudflare Workers/Pages
-- [x] AWS Lambda
-- [x] Bun
-- [x] Deno
-- [x] Azure Functions
-- [x] Azure Static Web Apps
+
## 設定
-`react-server.config.mjs`ファイルに `adapter` エントリを追加します。ビルトインアダプタの名前(`vercel`、`netlify`、`cloudflare`、`aws`、`bun`、`deno`、`azure`、または `azure-swa`)を文字列で指定するか、外部アダプタパッケージを使用できます。
+`react-server.config.mjs`ファイルに `adapter` エントリを追加します。ビルトインアダプタの名前(`vercel`、`netlify`、`cloudflare`、`aws`、`bun`、`deno`、`azure`、`azure-swa`、または `firebase`)を文字列で指定するか、外部アダプタパッケージを使用できます。
> **Note:** **Bun** または **Deno** でプロダクションビルドを実行すると、対応するアダプタが自動的に検出・使用されます。設定は不要です。明示的な `adapter` 設定またはCLIの `--adapter ` で上書きできます。自動検出を無効にするには `--no-adapter` を使用してください。
-> **Note:** `aws` アダプタには [AWS SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html) と設定済みのAWS認証情報が必要です。セットアップ手順は [AWS Lambda](/deploy/aws) ページを参照してください。
-
```mjs
export default {
adapter: 'vercel',
diff --git a/docs/src/pages/ja/(pages)/deploy/api.mdx b/docs/src/pages/ja/(pages)/deploy/api.mdx
index cc4f1e3e..cf0c2939 100644
--- a/docs/src/pages/ja/(pages)/deploy/api.mdx
+++ b/docs/src/pages/ja/(pages)/deploy/api.mdx
@@ -57,7 +57,7 @@ export default function defineConfig(adapterOptions) {
`handler`: アダプタハンドラ関数
-`deploy`: デプロイコマンドと引数。これはオプションです。指定された場合、アプリケーションをデプロイするために、ビルド後に開発者が実行する必要のあるコマンドを表示します。ビルド中に `--deploy` フラグが指定された場合、アダプタはこのコマンドを実行します。`deploy` プロパティは関数としても指定でき、アダプタオプション、CLI オプション、およびハンドラの結果を使用して呼び出されます。アダプタオプションやハンドラの結果に基づいてデプロイコマンドをカスタマイズする必要がある場合に便利です。`command` と `args` を含む結果を提供しない場合、デフォルトのデプロイ処理(コマンドの実行)はスキップされます。これは、アダプタ内でカスタムデプロイワークフローを実装したい場合に役立ちます。
+`deploy`: デプロイコマンドと引数。これはオプションです。指定された場合、アプリケーションをデプロイするために、ビルド後に開発者が実行する必要のあるコマンドを表示します。ビルド中に `--deploy` フラグが指定された場合、アダプタはこのコマンドを実行します。`--deploy` にJSON文字列を渡すこともできます(例: `--deploy '{"project": "my-project"}'`)。渡されたオプションは `adapterOptions` にマージされ、CLIの値が優先されます。`deploy` プロパティは関数としても指定でき、アダプタオプション、CLI オプション、およびハンドラの結果を使用して呼び出されます。アダプタオプションやハンドラの結果に基づいてデプロイコマンドをカスタマイズする必要がある場合に便利です。`command` と `args` を含む結果を提供しない場合、デフォルトのデプロイ処理(コマンドの実行)はスキップされます。これは、アダプタ内でカスタムデプロイワークフローを実装したい場合に役立ちます。
```js
export const adapter = createAdapter({
diff --git a/docs/src/pages/ja/(pages)/deploy/firebase.mdx b/docs/src/pages/ja/(pages)/deploy/firebase.mdx
new file mode 100644
index 00000000..9f683668
--- /dev/null
+++ b/docs/src/pages/ja/(pages)/deploy/firebase.mdx
@@ -0,0 +1,189 @@
+---
+title: Firebase
+category: Deploy
+order: 8
+---
+
+import Link from "../../../../components/Link.jsx";
+
+# Firebase Functions
+
+Firebase にデプロイするには、ビルトインの `firebase` アダプタを使用します。このアダプタは Firebase Cloud Functions(v2)と Firebase Hosting のリライトを使用し、サーバーを単一のエッジファイルにバンドルし、完全なレスポンスストリーミングをサポートします。
+
+
+## インストール
+
+
+[Firebase CLI](https://firebase.google.com/docs/cli) のインストールが必要です:
+
+```sh
+npm install -g firebase-tools
+
+# Firebase にログイン
+firebase login
+```
+
+追加パッケージは不要です — アダプタは `@lazarv/react-server` に組み込まれています。
+
+Firebase プロジェクトは Cloud Functions をデプロイするために **Blaze(従量課金)プラン** が必要です。[Firebase Console](https://console.firebase.google.com) でアップグレードできます。
+
+`react-server.config.mjs` ファイルにアダプタを追加します:
+
+```mjs
+export default {
+ adapter: "firebase",
+};
+```
+
+
+## 設定
+
+
+オプションを渡してアダプタをカスタマイズできます:
+
+```mjs
+export default {
+ adapter: [
+ "firebase",
+ {
+ project: "my-project", // Firebase プロジェクト ID
+ region: "us-central1", // 関数のリージョン
+ memory: "512MiB", // メモリ割り当て
+ timeoutSeconds: 60, // リクエストタイムアウト
+ minInstances: 0, // 最小ウォームインスタンス数
+ maxInstances: 100, // 最大同時インスタンス数
+ concurrency: 80, // インスタンスあたりのリクエスト数
+ hosting: {}, // firebase.json hosting の追加プロパティ
+ functions: {}, // firebase.json functions の追加プロパティ
+ firebase: {}, // firebase.json のトップレベル追加プロパティ
+ },
+ ],
+};
+```
+
+### 設定オプション
+
+- `project`: Firebase プロジェクト ID。`package.json` の name(スコーププレフィックスなし)にフォールバックします。
+- `region`: Cloud Functions のリージョン(デフォルト: `"us-central1"`)。[利用可能なリージョン](https://cloud.google.com/functions/docs/locations)を参照してください。
+- `memory`: 関数のメモリ割り当て(デフォルト: `"512MiB"`)。有効な値: `"128MiB"`, `"256MiB"`, `"512MiB"`, `"1GiB"`, `"2GiB"`, `"4GiB"`, `"8GiB"`, `"16GiB"`, `"32GiB"`。
+- `timeoutSeconds`: 最大リクエスト時間(秒)(デフォルト: `60`)。v2 関数の最大値は 540 です。
+- `minInstances`: ウォーム状態を維持する最小インスタンス数(デフォルト: `0`)。0 より大きく設定するとコールドスタートが減少しますが、コストが発生します。
+- `maxInstances`: 最大同時インスタンス数(デフォルト: `100`)。
+- `concurrency`: インスタンスあたりの最大同時リクエスト数(デフォルト: `80`)。
+- `hosting`: `firebase.json` の `hosting` セクションにマージする追加プロパティ。
+- `functions`: `firebase.json` の `functions` セクションにマージする追加プロパティ。
+- `firebase`: `firebase.json` にマージするトップレベルの追加プロパティ。
+
+デプロイ時に `--deploy` フラグでアダプタオプションを渡すこともできます:
+
+```sh
+pnpm react-server build --adapter firebase --deploy='{"project":"my-project"}'
+```
+
+
+## デプロイ
+
+
+ビルドとデプロイを一括で実行:
+
+```sh
+pnpm react-server build --deploy
+```
+
+または、先にビルドしてから手動でデプロイ:
+
+```sh
+# ビルド
+pnpm react-server build
+
+# Firebase CLI でデプロイ
+firebase deploy --only functions,hosting --project
+```
+
+
+## レスポンスストリーミング
+
+
+このアダプタはレスポンスストリーミングをサポートしています。React Server Components、Suspense バウンダリ、プログレッシブ HTML 配信はすべてそのまま動作します。生成された Cloud Function は、Node.js レスポンスオブジェクトの `write()` メソッドを使用してレスポンスボディをチャンクごとにストリーミングします。
+
+
+## 仕組み
+
+
+アダプタは**エッジビルド**モードを使用し、サーバー全体を単一のファイルにバンドルします。ビルド時に以下を行います:
+
+1. サーバーを `.firebase-app/server/.react-server/server/edge.mjs` にバンドル
+2. すべての静的アセットを `.firebase-app/public/` にコピー
+3. `src/index.mjs` ラッパーを生成:
+ - `firebase-functions/v2/https` から `onRequest` をインポート
+ - すべてのメソッドとパスに対応する HTTP トリガー関数を登録
+ - ビルド時のルートマップから `readFileSync()` で静的ファイルを配信
+ - 受信した Express スタイルのリクエストを標準 `Request` オブジェクトに変換
+ - 動的リクエストをバンドルされたエッジハンドラーに委任
+ - レスポンスをストリーミング
+4. `firebase-functions` と `firebase-admin` の依存関係を含む `package.json` を生成
+5. `.firebase-app/` で `npm install` を実行して依存関係をインストール
+6. 関数へのリライトを設定した `firebase.json` を生成
+7. プロジェクト ID が利用可能な場合 `.firebaserc` を生成
+
+> **重要:** `firebase-functions` と `firebase-admin` パッケージはエッジビルドにバンドルできません。Firebase Functions ランタイムはこれらを `node_modules` の外部依存関係として期待しています。
+
+
+## ローカル開発
+
+
+ビルド後、Firebase エミュレータを使用してローカルでテストできます:
+
+```sh
+firebase emulators:start --only functions,hosting
+```
+
+
+## 出力構造
+
+
+```
+.firebase-app/
+├── package.json # 依存関係 (firebase-functions, firebase-admin)
+├── node_modules/ # インストールされた依存関係
+├── public/ # 静的アセット (HTML, CSS, JS, 画像)
+├── server/
+│ └── .react-server/ # バンドルされたサーバー (edge.mjs, マニフェスト)
+└── src/
+ └── index.mjs # Firebase Cloud Function エントリ
+
+firebase.json # Firebase 設定 (Hosting + Functions)
+.firebaserc # Firebase プロジェクトエイリアス
+```
+
+
+## トラブルシューティング
+
+
+### 「Blaze プランが必要」エラー
+
+Firebase Cloud Functions には Blaze(従量課金)課金プランが必要です。[Firebase Console](https://console.firebase.google.com) でプロジェクトをアップグレードしてください。Blaze プランには寛大な無料枠が含まれており、無料枠を超えた実際の使用量に対してのみ課金されます。課金プランはプロジェクトごとに設定され、他の Firebase プロジェクトには影響しません。
+
+### コールドスタート
+
+デフォルトでは `minInstances` は `0` で、アイドル時に関数はゼロにスケールダウンします。ゼロからの最初のリクエストはコールドスタート(通常 1〜3 秒)を経験します。`minInstances: 1` に設定して 1 つのインスタンスをウォーム状態に保ちます:
+
+```mjs
+export default {
+ adapter: ["firebase", { minInstances: 1 }],
+};
+```
+
+### 「プロジェクトが見つかりません」エラー
+
+プロジェクト ID が正しいことを確認してください。Firebase プロジェクトの一覧は以下で確認できます:
+
+```sh
+firebase projects:list
+```
+
+アダプタオプションまたは `--deploy` フラグでプロジェクトを明示的に設定してください:
+
+```sh
+pnpm react-server build --adapter firebase --deploy='{"project":"my-project"}'
+```
diff --git a/docs/src/pages/ja/(pages)/framework/cli.mdx b/docs/src/pages/ja/(pages)/framework/cli.mdx
index 31a59aa5..c41b0c52 100644
--- a/docs/src/pages/ja/(pages)/framework/cli.mdx
+++ b/docs/src/pages/ja/(pages)/framework/cli.mdx
@@ -203,6 +203,14 @@ export default {
`react-server.config.mjs`ファイルでアダプターを使用している場合、アダプターは事前にビルドを行い最後にアプリケーションをデプロイします。
+`--deploy` にJSON文字列を渡すことで、デプロイ時にアダプター固有のオプションを指定することもできます。これらのオプションは設定ファイルの `adapterOptions` とマージされ、CLIの値が優先されます。
+
+```sh
+pnpm react-server build --deploy '{"project": "my-project"}'
+```
+
+これは、設定ファイルを変更せずにアダプターオプションを上書きまたは追加したい場合に便利です。例えば、デプロイ先のプロジェクトや環境を指定する際に使用できます。
+
### --eval
diff --git a/packages/react-server/adapters/core.mjs b/packages/react-server/adapters/core.mjs
index a6393b09..6259ced7 100644
--- a/packages/react-server/adapters/core.mjs
+++ b/packages/react-server/adapters/core.mjs
@@ -536,16 +536,22 @@ export async function getDependencies(adapterFiles, reactServerDir) {
}
export async function spawnCommand(command, args, options) {
+ const { cwd: spawnCwd, ...rest } = options ?? {};
const deploy = spawn(command, args, {
- cwd: options?.cwd ?? cwd,
+ cwd: spawnCwd ?? cwd,
stdio: "inherit",
+ ...rest,
});
await new Promise((resolve, reject) => {
deploy.on("exit", (code) => {
if (code === 0) {
resolve();
} else {
- reject();
+ reject(
+ new Error(
+ `Command "${command} ${args.join(" ")}" exited with code ${code}`
+ )
+ );
}
});
});
diff --git a/packages/react-server/adapters/firebase/functions/entry.mjs b/packages/react-server/adapters/firebase/functions/entry.mjs
new file mode 100644
index 00000000..c3c688a6
--- /dev/null
+++ b/packages/react-server/adapters/firebase/functions/entry.mjs
@@ -0,0 +1,53 @@
+import { reactServer } from "@lazarv/react-server/edge";
+import { createContext } from "@lazarv/react-server/http";
+
+let serverPromise = null;
+
+export default async (request, context) => {
+ try {
+ const url = new URL(request.url);
+
+ if (!serverPromise) {
+ serverPromise = reactServer({
+ origin: process.env.ORIGIN || `${url.protocol}//${url.host}`,
+ outDir: "../",
+ });
+ }
+
+ const { handler } = await serverPromise;
+
+ const origin = process.env.ORIGIN || `${url.protocol}//${url.host}`;
+ const httpContext = createContext(request, {
+ origin,
+ runtime: "firebase",
+ platformExtras: { firebaseContext: context },
+ });
+
+ const response = await handler(httpContext);
+
+ if (!response) {
+ return new Response("Not Found", { status: 404 });
+ }
+
+ // Add set-cookie headers
+ if (httpContext._setCookies?.length) {
+ const headers = new Headers(response.headers);
+ for (const c of httpContext._setCookies) {
+ headers.append("set-cookie", c);
+ }
+ return new Response(response.body, {
+ status: response.status,
+ statusText: response.statusText,
+ headers,
+ });
+ }
+
+ return response;
+ } catch (e) {
+ console.error("Request handler error:", e);
+ return new Response(e.message || "Internal Server Error", {
+ status: 500,
+ headers: { "Content-Type": "text/plain" },
+ });
+ }
+};
diff --git a/packages/react-server/adapters/firebase/index.mjs b/packages/react-server/adapters/firebase/index.mjs
new file mode 100644
index 00000000..3a2c1605
--- /dev/null
+++ b/packages/react-server/adapters/firebase/index.mjs
@@ -0,0 +1,342 @@
+import { existsSync, readFileSync } from "node:fs";
+import { dirname, join } from "node:path";
+import { fileURLToPath } from "node:url";
+import { mkdir, writeFile } from "node:fs/promises";
+
+import * as sys from "@lazarv/react-server/lib/sys.mjs";
+import {
+ banner,
+ createAdapter,
+ message,
+ spawnCommand,
+ success,
+ writeJSON,
+} from "@lazarv/react-server/adapters/core";
+
+const cwd = sys.cwd();
+const outDir = join(cwd, ".firebase-app");
+const outStaticDir = join(outDir, "public");
+const outServerDir = join(outDir, "server");
+const adapterDir = dirname(fileURLToPath(import.meta.url));
+
+function resolveAppName(adapterOptions) {
+ if (adapterOptions?.project) return adapterOptions.project;
+ const packageJsonPath = join(cwd, "package.json");
+ if (existsSync(packageJsonPath)) {
+ try {
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
+ return packageJson.name?.replace(/^@[^/]+\//, "");
+ } catch {
+ // Ignore parsing errors
+ }
+ }
+ return null;
+}
+
+/**
+ * Build options for the Firebase Functions adapter.
+ * Uses edge build to bundle the server into a single file.
+ */
+export const buildOptions = {
+ edge: {
+ entry: join(adapterDir, "functions/entry.mjs"),
+ },
+};
+
+export const adapter = createAdapter({
+ name: "Firebase Functions",
+ outDir,
+ outStaticDir,
+ outServerDir,
+ handler: async function ({
+ adapterOptions,
+ files,
+ _options,
+ reactServerOutDir,
+ }) {
+ // Collect all static file paths for the route map
+ banner("generating static file manifest", { emoji: "🗺️" });
+ const [staticFiles, assetFiles, clientFiles, publicFiles] =
+ await Promise.all([
+ files.static(),
+ files.assets(),
+ files.client(),
+ files.public(),
+ ]);
+
+ // Build the static file entries as a map from URL path to file path
+ const staticMap = {};
+ const addFile = (urlPath, filePath) => {
+ staticMap[urlPath] = filePath;
+ };
+
+ for (const f of staticFiles) {
+ addFile(`/${f}`, f);
+ if (f.endsWith("/index.html")) {
+ const dirPath = "/" + f.slice(0, -"/index.html".length);
+ addFile(dirPath || "/", f);
+ } else if (f === "index.html") {
+ addFile("/", f);
+ }
+ }
+ for (const f of assetFiles) addFile(`/${f}`, f);
+ for (const f of clientFiles) addFile(`/${f}`, f);
+ for (const f of publicFiles) addFile(`/${f}`, f);
+
+ success(`${Object.keys(staticMap).length} static files mapped`);
+
+ // Generate the Firebase Functions entry that bridges between
+ // Firebase Cloud Functions and the react-server edge handler.
+ banner("creating Firebase Functions entry", { emoji: "⚡" });
+
+ const staticMapJson = JSON.stringify(staticMap, null, 2);
+ const region = adapterOptions?.region ?? "us-central1";
+ const memory = adapterOptions?.memory ?? "512MiB";
+ const timeoutSeconds = adapterOptions?.timeoutSeconds ?? 60;
+ const minInstances = adapterOptions?.minInstances ?? 0;
+ const maxInstances = adapterOptions?.maxInstances ?? 100;
+ const concurrency = adapterOptions?.concurrency ?? 80;
+
+ const functionEntry = `import { onRequest } from "firebase-functions/v2/https";
+import { readFileSync } from "node:fs";
+import { dirname, join, extname } from "node:path";
+import { fileURLToPath } from "node:url";
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+const staticDir = join(__dirname, "../public");
+const serverDir = join(__dirname, "../server/${reactServerOutDir}");
+
+const MIME_TYPES = {
+ ".html": "text/html; charset=utf-8",
+ ".css": "text/css; charset=utf-8",
+ ".js": "text/javascript; charset=utf-8",
+ ".mjs": "text/javascript; charset=utf-8",
+ ".json": "application/json; charset=utf-8",
+ ".png": "image/png",
+ ".jpg": "image/jpeg",
+ ".jpeg": "image/jpeg",
+ ".gif": "image/gif",
+ ".svg": "image/svg+xml",
+ ".ico": "image/x-icon",
+ ".webp": "image/webp",
+ ".avif": "image/avif",
+ ".woff": "font/woff",
+ ".woff2": "font/woff2",
+ ".ttf": "font/ttf",
+ ".otf": "font/otf",
+ ".eot": "application/vnd.ms-fontobject",
+ ".xml": "application/xml",
+ ".txt": "text/plain; charset=utf-8",
+ ".map": "application/json",
+ ".webmanifest": "application/manifest+json",
+ ".mp4": "video/mp4",
+ ".webm": "video/webm",
+ ".mp3": "audio/mpeg",
+ ".wav": "audio/wav",
+ ".pdf": "application/pdf",
+ ".wasm": "application/wasm",
+};
+
+const STATIC_FILES = ${staticMapJson};
+
+const CACHE_IMMUTABLE = "public, max-age=31536000, immutable";
+
+process.chdir(serverDir);
+const edgeHandler = (await import("../server/${reactServerOutDir}/server/edge.mjs")).default;
+
+export const server = onRequest(
+ {
+ region: "${region}",
+ memory: "${memory}",
+ timeoutSeconds: ${timeoutSeconds},
+ minInstances: ${minInstances},
+ maxInstances: ${maxInstances},
+ concurrency: ${concurrency},
+ invoker: "public",
+ },
+ async (req, res) => {
+ try {
+ const url = new URL(req.url, \`\${req.protocol}://\${req.headers.host || req.hostname}\`);
+ const pathname = decodeURIComponent(url.pathname);
+
+ // Try to serve static files first
+ const staticFile = STATIC_FILES[pathname];
+ if (staticFile) {
+ const filePath = join(staticDir, staticFile);
+ const ext = extname(staticFile);
+ const contentType = MIME_TYPES[ext] || "application/octet-stream";
+ const body = readFileSync(filePath);
+
+ res.set("Content-Type", contentType);
+ res.set("Content-Length", body.length.toString());
+
+ if (pathname.startsWith("/assets/") || pathname.startsWith("/client/")) {
+ res.set("Cache-Control", CACHE_IMMUTABLE);
+ }
+
+ res.status(200).send(body);
+ return;
+ }
+
+ // Build a standard Request object for the edge handler
+ const headers = new Headers();
+ for (const [key, value] of Object.entries(req.headers)) {
+ if (value !== undefined) {
+ if (Array.isArray(value)) {
+ for (const v of value) headers.append(key, v);
+ } else {
+ headers.set(key, value);
+ }
+ }
+ }
+
+ const requestInit = {
+ method: req.method,
+ headers,
+ };
+
+ // Include body for non-GET/HEAD requests
+ if (req.method !== "GET" && req.method !== "HEAD" && req.rawBody) {
+ requestInit.body = req.rawBody;
+ }
+
+ const request = new Request(url.toString(), requestInit);
+ const response = await edgeHandler(request);
+
+ // Write response status and headers
+ res.status(response.status);
+ response.headers.forEach((value, key) => {
+ res.set(key, value);
+ });
+
+ // Stream the response body
+ if (response.body) {
+ const reader = response.body.getReader();
+ try {
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) break;
+ res.write(value);
+ }
+ } finally {
+ reader.releaseLock();
+ }
+ }
+
+ res.end();
+ } catch (e) {
+ console.error("Request handler error:", e);
+ res.status(500).set("Content-Type", "text/plain").send(e.message || "Internal Server Error");
+ }
+ }
+);
+`;
+
+ const srcDir = join(outDir, "src");
+ await mkdir(srcDir, { recursive: true });
+ message("creating", "src/index.mjs");
+ await writeFile(join(srcDir, "index.mjs"), functionEntry);
+ success("Firebase Functions entry created");
+
+ banner("creating Firebase configuration", { emoji: "⚙️" });
+
+ const appName = resolveAppName(adapterOptions);
+
+ // Generate package.json for the function
+ message("creating", "package.json");
+ await writeJSON(join(outDir, "package.json"), {
+ name: appName ? `${appName}-functions` : "react-server-firebase",
+ private: true,
+ type: "module",
+ main: "src/index.mjs",
+ engines: {
+ node: ">=22",
+ },
+ dependencies: {
+ "firebase-functions": "^7.0.0",
+ "firebase-admin": "^13.0.0",
+ },
+ });
+ success("package.json created");
+
+ // Install firebase-functions and firebase-admin — these packages CANNOT be
+ // bundled because the Firebase Functions runtime expects them as external
+ // dependencies.
+ // Strip pnpm-injected npm_config_* env vars that cause warnings in npm 11+.
+ const cleanEnv = Object.fromEntries(
+ Object.entries(process.env).filter(
+ ([k]) => !k.startsWith("npm_config_") && !k.startsWith("pnpm_config_")
+ )
+ );
+ message("installing", "firebase-functions and firebase-admin");
+ await spawnCommand("npm", ["install", "--prefix", outDir], {
+ env: cleanEnv,
+ });
+ success("dependencies installed");
+
+ // Generate firebase.json configuration
+ message("creating", "firebase.json");
+ const firebaseJson = {
+ hosting: {
+ public: ".firebase-app/public",
+ ignore: ["firebase.json", "**/.*", "**/node_modules/**"],
+ rewrites: [
+ {
+ source: "**",
+ function: {
+ functionId: "server",
+ region: region,
+ },
+ },
+ ],
+ ...adapterOptions?.hosting,
+ },
+ functions: {
+ source: ".firebase-app",
+ runtime: "nodejs22",
+ ...adapterOptions?.functions,
+ },
+ ...adapterOptions?.firebase,
+ };
+ await writeJSON(join(cwd, "firebase.json"), firebaseJson);
+ success("firebase.json created");
+
+ // Generate .firebaserc if project name is available
+ if (appName) {
+ message("creating", ".firebaserc");
+ await writeJSON(join(cwd, ".firebaserc"), {
+ projects: {
+ default: appName,
+ },
+ });
+ success(".firebaserc created");
+ }
+ },
+ deploy: async ({ adapterOptions, _options }) => {
+ const project = adapterOptions?.project ?? resolveAppName(adapterOptions);
+
+ if (!project) {
+ return {
+ command: "firebase",
+ args: ["deploy", "--only", "functions,hosting"],
+ message:
+ " Set your Firebase project via adapter options:\n" +
+ ' adapter: ["firebase", { project: "my-project" }]\n' +
+ ' or add a "name" field to your package.json.\n' +
+ " Install Firebase CLI: npm i -g firebase-tools",
+ };
+ }
+
+ return {
+ command: "firebase",
+ args: ["deploy", "--only", "functions,hosting", "--project", project],
+ afterDeploy: () => {
+ banner(`deployed to https://${project}.web.app`, { emoji: "🔥" });
+ },
+ };
+ },
+});
+
+export default function defineConfig(adapterOptions) {
+ return async (_, root, options) => adapter(adapterOptions, root, options);
+}
diff --git a/packages/react-server/bin/commands/build.mjs b/packages/react-server/bin/commands/build.mjs
index bf104ecf..6901139d 100644
--- a/packages/react-server/bin/commands/build.mjs
+++ b/packages/react-server/bin/commands/build.mjs
@@ -19,7 +19,11 @@ export default (cli) =>
default: "",
type: [String],
})
- .option("--deploy", "[boolean] deploy using adapter", { default: false })
+ .option(
+ "--deploy [options]",
+ "[boolean|json] deploy using adapter, optionally pass JSON adapter options",
+ { default: false }
+ )
.option("-e, --eval ", "evaluate code", { type: "string" })
.option("--outDir ", "[string] output directory", {
default: ".react-server",
diff --git a/packages/react-server/lib/build/adapter.mjs b/packages/react-server/lib/build/adapter.mjs
index 89c41c84..110377bc 100644
--- a/packages/react-server/lib/build/adapter.mjs
+++ b/packages/react-server/lib/build/adapter.mjs
@@ -105,11 +105,14 @@ export async function getAdapterBuildOptions(config, options) {
if (!adapterModule) return {};
+ const deployOptions = parseDeployOptions(options.deploy);
+ const mergedOptions = { ...adapterOptions, ...deployOptions };
+
const adapterExports = await loadAdapterModule(adapterModule);
const { buildOptions } = adapterExports;
if (typeof buildOptions === "function") {
- return (await buildOptions(adapterOptions, options)) ?? {};
+ return (await buildOptions(mergedOptions, options)) ?? {};
}
return buildOptions ?? {};
@@ -118,19 +121,34 @@ export async function getAdapterBuildOptions(config, options) {
}
}
+function parseDeployOptions(deploy) {
+ if (!deploy || typeof deploy === "boolean") return {};
+ if (typeof deploy === "string") {
+ try {
+ return JSON.parse(deploy);
+ } catch {
+ return {};
+ }
+ }
+ return {};
+}
+
export default async function adapter(root, options) {
const config = getContext(CONFIG_CONTEXT)?.[CONFIG_ROOT] ?? {};
+ const deployOptions = parseDeployOptions(options.deploy);
+
const adapter = resolveAdapter(config, options);
if (adapter) {
if (typeof adapter === "function") {
- return await adapter({}, root, options);
+ return await adapter(deployOptions, root, options);
}
const [adapterModule, adapterOptions] =
typeof adapter === "string" ? [adapter] : adapter;
if (adapterModule) {
+ const mergedOptions = { ...adapterOptions, ...deployOptions };
const { adapter: adapterFn } = await loadAdapterModule(adapterModule);
- await adapterFn(adapterOptions, root, options);
+ await adapterFn(mergedOptions, root, options);
}
} else if (options.deploy) {
console.log(colors.yellow("No adapter configured. Skipping deployment."));
diff --git a/packages/react-server/package.json b/packages/react-server/package.json
index efb8e781..63e4f288 100644
--- a/packages/react-server/package.json
+++ b/packages/react-server/package.json
@@ -142,6 +142,10 @@
"types": "./adapters/adapter.d.ts",
"default": "./adapters/azure-swa/index.mjs"
},
+ "./adapters/firebase": {
+ "types": "./adapters/adapter.d.ts",
+ "default": "./adapters/firebase/index.mjs"
+ },
"./worker": {
"types": "./worker/index.d.ts",
"default": "./worker/index.mjs"