next

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Next.js Integration

Next.js 集成

The
@emulators/adapter-next
package embeds emulators directly into a Next.js App Router app, running them on the same origin. This is particularly useful for Vercel preview deployments where OAuth callback URLs change with every deployment.
@emulators/adapter-next
包可将模拟器直接嵌入Next.js App Router应用中,使其在同源环境下运行。这在Vercel预览部署中尤为实用,因为此类部署的OAuth回调URL会随每次部署而变化。

Install

安装

bash
npm install @emulators/adapter-next @emulators/github @emulators/google
Only install the emulators you need. Each
@emulators/*
package is published independently, keeping serverless bundles small.
bash
npm install @emulators/adapter-next @emulators/github @emulators/google
仅安装你需要的模拟器。每个
@emulators/*
包都是独立发布的,从而保持无服务器包体积小巧。

Route Handler

路由处理器

Create a catch-all route that serves emulator traffic:
typescript
// 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 } = 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: 'test@example.com', name: 'Test User' }],
      },
    },
  },
})
This creates the following routes:
  • /emulate/github/**
    serves the GitHub emulator
  • /emulate/google/**
    serves the Google emulator
创建一个兜底路由来处理模拟器流量:
typescript
// 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 } = 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: 'test@example.com', name: 'Test User' }],
      },
    },
  },
})
这将创建以下路由:
  • /emulate/github/**
    用于提供GitHub模拟器服务
  • /emulate/google/**
    用于提供Google模拟器服务

Auth.js / NextAuth Configuration

Auth.js / NextAuth 配置

Point your provider at the emulator paths on the same origin:
typescript
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` },
})
No
oauth_apps
need to be seeded. When none are configured, the emulator skips
client_id
,
client_secret
, and
redirect_uri
validation.
将你的提供者指向同源环境下的模拟器路径:
typescript
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
数据。当未配置任何
oauth_apps
时,模拟器会跳过
client_id
client_secret
redirect_uri
的验证。

Font Tracing for Serverless

无服务器环境下的字体追踪

Emulator UI pages use bundled fonts. Wrap your Next.js config to include them in the serverless trace:
typescript
// next.config.mjs
import { withEmulate } from '@emulators/adapter-next'

export default withEmulate({
  // your normal Next.js config
})
If you mount the catch-all at a custom path, pass the matching prefix:
typescript
export default withEmulate(nextConfig, { routePrefix: '/api/emulate' })
模拟器UI页面使用打包后的字体。请包装你的Next.js配置,将这些字体包含在无服务器追踪范围内:
typescript
// next.config.mjs
import { withEmulate } from '@emulators/adapter-next'

export default withEmulate({
  // 你的常规Next.js配置
})
如果你将兜底路由挂载在自定义路径下,请传入匹配的前缀:
typescript
export default withEmulate(nextConfig, { routePrefix: '/api/emulate' })

Persistence

持久化

By default, emulator state is in-memory and resets on every cold start. To persist state across restarts, pass a
persistence
adapter.
默认情况下,模拟器状态存储在内存中,每次冷启动时都会重置。若要在重启后保留状态,请传入一个
persistence
适配器。

Custom Adapter (Vercel KV, Redis, etc.)

自定义适配器(Vercel KV、Redis等)

typescript
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 } = createEmulateHandler({
  services: { github: { emulator: github } },
  persistence: kvAdapter,
})
typescript
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 } = createEmulateHandler({
  services: { github: { emulator: github } },
  persistence: kvAdapter,
})

File Persistence (Local Dev)

文件持久化(本地开发)

For local development,
@emulators/core
ships a file-based adapter:
typescript
import { filePersistence } from '@emulators/core'

// persists to a JSON file
persistence: filePersistence('.emulate/state.json'),
针对本地开发,
@emulators/core
提供了一个基于文件的适配器:
typescript
import { filePersistence } from '@emulators/core'

// 持久化到JSON文件
persistence: filePersistence('.emulate/state.json'),

How Persistence Works

持久化工作原理

  • Cold start: The adapter loads state from the persistence adapter. If found, it restores the full Store and token map (skipping seed). If not found, it seeds from config and saves the initial state.
  • After mutating requests (POST, PUT, PATCH, DELETE): State is saved. Saves are serialized via an internal queue to prevent race conditions.
  • No persistence configured: Falls back to pure in-memory. Seed data re-initializes on every cold start.
  • 冷启动:适配器从持久化加载状态。如果找到状态,则恢复完整的Store和令牌映射(跳过预填充数据)。如果未找到,则从配置中预填充数据并保存初始状态。
  • 变更请求后(POST、PUT、PATCH、DELETE):状态会被保存。保存操作通过内部队列序列化,以防止竞争条件。
  • 未配置持久化:回退到纯内存存储。预填充数据会在每次冷启动时重新初始化。

How It Works

工作原理

  1. Incoming request:
    /emulate/github/login/oauth/authorize?client_id=...
  2. Parse: service =
    github
    , rest =
    /login/oauth/authorize
  3. Strip prefix: A new
    Request
    is created with the stripped path and forwarded to the GitHub Hono app
  4. Rewrite response: HTML
    action
    and
    href
    attributes, CSS
    url()
    font references, and
    Location
    headers get the service prefix prepended
  5. Persist: After mutating requests, state is saved via the persistence adapter
  1. 传入请求
    /emulate/github/login/oauth/authorize?client_id=...
  2. 解析:服务 =
    github
    ,剩余路径 =
    /login/oauth/authorize
  3. 移除前缀:创建一个新的
    Request
    对象,移除前缀后转发到GitHub Hono应用
  4. 重写响应:HTML的
    action
    href
    属性、CSS的
    url()
    字体引用,以及
    Location
    头都会被添加服务前缀
  5. 持久化:变更请求完成后,状态通过持久化适配器保存

Limitations

限制

  • Requires the Node.js runtime (not Edge) since emulators use
    crypto.randomBytes
  • Concurrent serverless instances writing to the same persistence adapter use last-write-wins semantics (acceptable for dev/preview traffic)
  • 需要Node.js运行时(不支持Edge),因为模拟器使用
    crypto.randomBytes
  • 多个无服务器实例同时写入同一个持久化适配器时,采用最后写入优先的语义(适用于开发/预览流量)

Config Reference

配置参考

createEmulateHandler(config)

createEmulateHandler(config)

FieldTypeDescription
services
Record<string, EmulatorEntry>
Map of service name to emulator config
persistence?
PersistenceAdapter
Optional persistence adapter for state across cold starts
Each
EmulatorEntry
:
FieldTypeDescription
emulator
EmulatorModule
The emulator package (e.g.
import * as github from '@emulators/github'
)
seed?
Record<string, unknown>
Seed data matching the service's config schema
字段类型描述
services
Record<string, EmulatorEntry>
服务名称到模拟器配置的映射
persistence?
PersistenceAdapter
可选的持久化适配器,用于跨冷启动保留状态
每个
EmulatorEntry
字段类型描述
emulator
EmulatorModule
模拟器包(例如
import * as github from '@emulators/github'
seed?
Record<string, unknown>
符合服务配置 schema 的预填充数据

withEmulate(nextConfig, options?)

withEmulate(nextConfig, options?)

Wraps a Next.js config to include emulator font files in the serverless output trace. Call it around your exported config in
next.config.mjs
or
next.config.ts
.
OptionTypeDefaultDescription
routePrefix
string
"/emulate"
The path prefix where the catch-all route is mounted
包装Next.js配置,将模拟器字体文件包含在无服务器输出追踪中。在
next.config.mjs
next.config.ts
中,将导出的配置用此方法包裹。
选项类型默认值描述
routePrefix
string
"/emulate"
兜底路由挂载的路径前缀

PersistenceAdapter

PersistenceAdapter

typescript
interface PersistenceAdapter {
  load(): Promise<string | null>
  save(data: string): Promise<void>
}
The built-in
filePersistence(path)
from
@emulators/core
provides a file-based adapter for local development.
typescript
interface PersistenceAdapter {
  load(): Promise<string | null>
  save(data: string): Promise<void>
}
@emulators/core
内置的
filePersistence(path)
提供了一个基于文件的适配器,适用于本地开发。",