next について
@emulators/adapter-nextは、APIエミュレーターをNext.js App Routerアプリに組み込み、同一オリジンのパスでリクエストを処理します。プレビュー環境でOAuthをテストする開発者向けで、状態の永続化にも対応します。
主な機能
- 01Next.js App Routerへのエミュレーター組み込み
- 02同一オリジンで複数エミュレーターをルーティング
- 03永続化アダプターに対応
- 04変更リクエスト後の状態保存
- 05serverless出力のフォント追跡
活用シーン
- 01VercelプレビューでOAuthコールバックをテスト
- 02Next.jsのルートに複数のAPIエミュレーターを配置
- 03カスタムアダプターでコールドスタート間の状態を保持
- 04ローカル開発で状態をJSONファイルに保存
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ vercel-labs/ emulate --skill nextインストール後、Claude Code で/nextを入力して読み込まれたか確認します。
詳細
Next.js の統合
@emulators/adapter-next パッケージは、エミュレーターを Next.js App Router アプリに直接組み込み、同一オリジン上で実行します。これは、デプロイごとに OAuth コールバック URL が変わる Vercel のプレビューデプロイで特に便利です。
インストール
npm install @emulators/adapter-next @emulators/github @emulators/google
必要なエミュレーターのみをインストールしてください。各 @emulators/* パッケージは個別に公開されるため、サーバーレスバンドルを小さく保てます。
Route Handler
エミュレーターのトラフィックを処理するキャッチオールルートを作成します。
// app/emulate/[...path]/route.ts
import { createEmulateHandler } from '@emulators/adapter-next'
import * as github from '@emulators/github'
import * as google from '@emulators/google'
export const { GET, POST, PUT, PATCH, DELETE, OPTIONS } = createEmulateHandler({
services: {
github: {
emulator: github,
seed: {
users: [{ login: 'octocat', name: 'The Octocat' }],
repos: [{ owner: 'octocat', name: 'hello-world', auto_init: true }],
},
},
google: {
emulator: google,
seed: {
users: [{ email: '[email protected]', name: 'Test User' }],
},
},
},
})
次のルートが作成されます。
/emulate/github/**は GitHub エミュレーターを提供します/emulate/google/**は Google エミュレーターを提供します
GitHub のインストールトークンのメタデータは、サーバー側で /emulate/github/_emulate/installation-tokens から利用できます。
Auth.js / NextAuth の設定
プロバイダーが同一オリジン上のエミュレーターパスを参照するように設定します。
import GitHub from 'next-auth/providers/github'
const baseUrl = process.env.VERCEL_URL
? `https://${process.env.VERCEL_URL}`
: 'http://localhost:3000'
GitHub({
clientId: 'any-value',
clientSecret: 'any-value',
authorization: { url: `${baseUrl}/emulate/github/login/oauth/authorize` },
token: { url: `${baseUrl}/emulate/github/login/oauth/access_token` },
userinfo: { url: `${baseUrl}/emulate/github/user` },
})
oauth_apps をシードする必要はありません。設定されていない場合、エミュレーターは client_id、client_secret、redirect_uri の検証を省略します。
サーバーレス向けのフォントトレース
エミュレーターの UI ページでは、同梱されたフォントを使用します。サーバーレスのトレースにフォントを含めるには、Next.js の設定をラップしてください。
// next.config.mjs
import { withEmulate } from '@emulators/adapter-next'
export default withEmulate({
// your normal Next.js config
})
キャッチオールルートをカスタムパスにマウントする場合は、一致するプレフィックスを渡してください。
export default withEmulate(nextConfig, { routePrefix: '/api/emulate' })
永続化
デフォルトでは、エミュレーターの状態はメモリ内に保持され、コールドスタートのたびにリセットされます。再起動後も状態を保持するには、persistence アダプターを渡してください。
カスタムアダプター(Vercel KV、Redis など)
import { createEmulateHandler } from '@emulators/adapter-next'
import * as github from '@emulators/github'
const kvAdapter = {
async load() { return await kv.get('emulate-state') },
async save(data: string) { await kv.set('emulate-state', data) },
}
export const { GET, POST, PUT, PATCH, DELETE, OPTIONS } = createEmulateHandler({
services: { github: { emulator: github } },
persistence: kvAdapter,
})
ファイルによる永続化(ローカル開発)
ローカル開発向けに、@emulators/core にはファイルベースのアダプターが含まれています。
import { filePersistence } from '@emulators/core'
// persists to a JSON file
persistence: filePersistence('.emulate/state.json'),
GitHub App のシードでは private_key を省略できます。ハンドラーを保持し、サーバー専用の generatedSecrets() を呼び出してください。明示的に指定したキーは対象外です。永続化されたスナップショットは非公開に保ち、initialize をアトミックに実装してください。
永続化の仕組み
- コールドスタート時:アダプターが永続化アダプターから状態を読み込みます。状態が見つかった場合は、完全な Store とトークンマップを復元します(シード処理は省略されます)。見つからない場合は、設定からシードして初期状態を保存します。
- 状態を変更するリクエストの後(POST、PUT、PATCH、DELETE):状態が保存されます。競合状態を防ぐため、保存処理は内部キューを介して順番に実行されます。
- 永続化が設定されていない場合:純粋なメモリ内処理にフォールバックします。シードデータはコールドスタートのたびに再初期化されます。
仕組み
- 受信リクエスト:
/emulate/github/login/oauth/authorize?client_id=... - 解析:service =
github、rest =/login/oauth/authorize - プレフィックスの除去:パスからプレフィックスを除いた新しい
Requestを作成し、GitHub サービスアプリに転送します - レスポンスの書き換え:HTML の
action属性とhref属性、CSS のurl()フォント参照、Locationヘッダーにサービスプレフィックスを付加します - 永続化:状態を変更するリクエストの後、永続化アダプターを介して状態を保存します
制限事項
- エミュレーターが
crypto.randomBytesを使用するため、Node.js ランタイムが必要です(Edge は不可) - 同時変更は後勝ちのセマンティクスで処理されます。生成された ID を使用する場合、コールドスタートをまたいで初期スナップショットをアトミックに選択するために
initializeが必要です。
設定リファレンス
createEmulateHandler(config)
| フィールド | 型 | 説明 |
|---|---|---|
services | Record<string, EmulatorEntry> | サービス名とエミュレーター設定のマップ |
persistence? | PersistenceAdapter | コールドスタートをまたいで状態を保持するためのオプションの永続化アダプター |
各 EmulatorEntry:
| フィールド | 型 | 説明 |
|---|---|---|
emulator | EmulatorModule | エミュレーターパッケージ(例:import * as github from '@emulators/github') |
seed? | Record<string, unknown> | サービスの設定スキーマに一致するシードデータ |
withEmulate(nextConfig, options?)
エミュレーターのフォントファイルをサーバーレス出力トレースに含めるために、Next.js の設定をラップします。next.config.mjs または next.config.ts で、エクスポートする設定をこの関数で囲んでください。
| オプション | 型 | デフォルト | 説明 |
|---|---|---|---|
routePrefix | string | "/emulate" | キャッチオールルートをマウントするパスプレフィックス |
PersistenceAdapter
interface PersistenceAdapter {
load(): Promise<string | null>
save(data: string): Promise<void>
initialize?(data: string): Promise<string>
}
initialize は初期値をアトミックに作成するか、ほかのインスタンスが先に作成した値を返す必要があります。Redis の SET NX のような compare-and-set セマンティクスで実装してください。@emulators/core の組み込み filePersistence(path) は、ローカル開発向けにこの動作を提供します。
カスタムエミュレーター
アダプターは、services[name].emulator 内の emulate の defineEmulator で作成された定義を受け付けます。CLI/テストの定義を再利用してください。カスタムインスペクターはオプトインです。ルート相対のリダイレクト先はサービスのマウント配下に留まり、カスタム HTML ボディはそのまま通過します。テストの終了処理では、返されたハンドラーの close() を await してください。デプロイする定義では、emulate をランタイム依存関係としてインストールしてください。シード、スナップショット、永続化、ライフサイクルのルールについては https://emulate.dev/docs/custom-emulators を参照してください。