vercel-labs-emulate

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

vercel-labs/emulate

vercel-labs/emulate

Skill by ara.so — Daily 2026 Skills collection.
emulate
provides fully stateful, production-fidelity local HTTP servers that replace Vercel, GitHub, and Google APIs. Designed for CI pipelines and no-network sandboxes — not mocks, real in-memory state with proper pagination, OAuth, webhooks, and cascading deletes.
ara.so开发的Skill — 属于Daily 2026 Skills系列。
emulate
提供具备完整状态、与生产环境保真度一致的本地HTTP服务器,可替代Vercel、GitHub和Google的API。专为CI流水线与无网络沙箱场景设计——并非简单的Mock,而是拥有内存状态,支持分页、OAuth、Webhook和级联删除等完整功能。

Installation

安装

bash
undefined
bash
undefined

CLI (no install needed)

CLI(无需安装)

npx emulate
npx emulate

Or install as a dev dependency

或作为开发依赖安装

npm install --save-dev emulate
undefined
npm install --save-dev emulate
undefined

CLI Usage

CLI 使用方式

bash
undefined
bash
undefined

Start all services with defaults

使用默认配置启动所有服务

npx emulate
npx emulate

Start specific services

启动指定服务

npx emulate --service vercel,github
npx emulate --service vercel,github

Custom base port (auto-increments per service)

自定义基础端口(每个服务自动递增端口号)

npx emulate --port 3000
npx emulate --port 3000

Start with seed data

携带初始数据启动

npx emulate --seed emulate.config.yaml
npx emulate --seed emulate.config.yaml

Generate a starter config

生成初始配置文件

npx emulate init
npx emulate init

Generate config for a specific service

生成指定服务的配置文件

npx emulate init --service github
npx emulate init --service github

List available services

列出可用服务

npx emulate list

Default ports:
- **Vercel** → `http://localhost:4000`
- **GitHub** → `http://localhost:4001`
- **Google** → `http://localhost:4002`

Port can also be set via `EMULATE_PORT` or `PORT` environment variables.
npx emulate list

默认端口:
- **Vercel** → `http://localhost:4000`
- **GitHub** → `http://localhost:4001`
- **Google** → `http://localhost:4002`

端口也可通过`EMULATE_PORT`或`PORT`环境变量设置。

Programmatic API

程序化API

typescript
import { createEmulator, type Emulator } from 'emulate'

// Start a single service
const github = await createEmulator({ service: 'github', port: 4001 })
const vercel = await createEmulator({ service: 'vercel', port: 4002 })

console.log(github.url)  // 'http://localhost:4001'
console.log(vercel.url)  // 'http://localhost:4002'

// Reset state (replays seed data)
github.reset()

// Shutdown
await github.close()
await vercel.close()
typescript
import { createEmulator, type Emulator } from 'emulate'

// 启动单个服务
const github = await createEmulator({ service: 'github', port: 4001 })
const vercel = await createEmulator({ service: 'vercel', port: 4002 })

console.log(github.url)  // 'http://localhost:4001'
console.log(vercel.url)  // 'http://localhost:4002'

// 重置状态(重新加载初始数据)
github.reset()

// 关闭服务
await github.close()
await vercel.close()

Options

配置选项

OptionDefaultDescription
service
(required)
'github'
,
'vercel'
, or
'google'
port
4000
Port for the HTTP server
seed
noneInline seed data object (same shape as YAML config)
选项默认值描述
service
(必填)
'github'
'vercel'
'google'
port
4000
HTTP服务器端口
seed
内联初始数据对象(与YAML配置格式一致)

Instance Methods

实例方法

MethodDescription
url
Base URL of the running server
reset()
Wipe in-memory store and replay seed data
close()
Shut down the server (returns Promise)
方法描述
url
运行中服务器的基础URL
reset()
清空内存存储并重新加载初始数据
close()
关闭服务器(返回Promise)

Vitest / Jest Setup

Vitest / Jest 配置

typescript
// vitest.setup.ts
import { createEmulator, type Emulator } from 'emulate'

let github: Emulator
let vercel: Emulator

beforeAll(async () => {
  ;[github, vercel] = await Promise.all([
    createEmulator({ service: 'github', port: 4001 }),
    createEmulator({ service: 'vercel', port: 4002 }),
  ])
  process.env.GITHUB_URL = github.url
  process.env.VERCEL_URL = vercel.url
})

afterEach(() => {
  github.reset()
  vercel.reset()
})

afterAll(() => Promise.all([github.close(), vercel.close()]))
typescript
// vitest.config.ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    setupFiles: ['./vitest.setup.ts'],
    environment: 'node',
  },
})
typescript
// vitest.setup.ts
import { createEmulator, type Emulator } from 'emulate'

let github: Emulator
let vercel: Emulator

beforeAll(async () => {
  ;[github, vercel] = await Promise.all([
    createEmulator({ service: 'github', port: 4001 }),
    createEmulator({ service: 'vercel', port: 4002 }),
  ])
  process.env.GITHUB_URL = github.url
  process.env.VERCEL_URL = vercel.url
})

afterEach(() => {
  github.reset()
  vercel.reset()
})

afterAll(() => Promise.all([github.close(), vercel.close()]))
typescript
// vitest.config.ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    setupFiles: ['./vitest.setup.ts'],
    environment: 'node',
  },
})

Seed Configuration

初始数据配置

Create
emulate.config.yaml
in your project root (auto-detected):
yaml
undefined
在项目根目录创建
emulate.config.yaml
(会被自动识别):
yaml
undefined

Auth tokens

认证令牌

tokens: my_token: login: admin scopes: [repo, user]
vercel: users: - username: developer name: Developer email: dev@example.com teams: - slug: my-team name: My Team projects: - name: my-app team: my-team framework: nextjs
github: users: - login: octocat name: The Octocat email: octocat@github.com orgs: - login: my-org name: My Organization repos: - owner: octocat name: hello-world language: JavaScript auto_init: true
google: users: - email: testuser@example.com name: Test User oauth_clients: - client_id: my-client-id.apps.googleusercontent.com client_secret: $GOOGLE_CLIENT_SECRET redirect_uris: - http://localhost:3000/api/auth/callback/google
undefined
tokens: my_token: login: admin scopes: [repo, user]
vercel: users: - username: developer name: Developer email: dev@example.com teams: - slug: my-team name: My Team projects: - name: my-app team: my-team framework: nextjs
github: users: - login: octocat name: The Octocat email: octocat@github.com orgs: - login: my-org name: My Organization repos: - owner: octocat name: hello-world language: JavaScript auto_init: true
google: users: - email: testuser@example.com name: Test User oauth_clients: - client_id: my-client-id.apps.googleusercontent.com client_secret: $GOOGLE_CLIENT_SECRET redirect_uris: - http://localhost:3000/api/auth/callback/google
undefined

Inline Seed (Programmatic)

内联初始数据(程序化方式)

typescript
const github = await createEmulator({
  service: 'github',
  port: 4001,
  seed: {
    users: [
      { login: 'testuser', name: 'Test User', email: 'test@example.com' }
    ],
    repos: [
      { owner: 'testuser', name: 'my-repo', language: 'TypeScript', auto_init: true }
    ],
  },
})
typescript
const github = await createEmulator({
  service: 'github',
  port: 4001,
  seed: {
    users: [
      { login: 'testuser', name: 'Test User', email: 'test@example.com' }
    ],
    repos: [
      { owner: 'testuser', name: 'my-repo', language: 'TypeScript', auto_init: true }
    ],
  },
})

OAuth Configuration

OAuth 配置

GitHub OAuth Apps

GitHub OAuth 应用

yaml
github:
  oauth_apps:
    - client_id: $GITHUB_CLIENT_ID
      client_secret: $GITHUB_CLIENT_SECRET
      name: My Web App
      redirect_uris:
        - http://localhost:3000/api/auth/callback/github
Without
oauth_apps
configured, the emulator accepts any
client_id
(backward-compatible). With apps configured, strict validation is enforced.
yaml
github:
  oauth_apps:
    - client_id: $GITHUB_CLIENT_ID
      client_secret: $GITHUB_CLIENT_SECRET
      name: My Web App
      redirect_uris:
        - http://localhost:3000/api/auth/callback/github
若未配置
oauth_apps
,模拟器会接受任意
client_id
(向后兼容)。配置后则会启用严格校验。

GitHub Apps (JWT Auth)

GitHub Apps(JWT 认证)

yaml
github:
  apps:
    - app_id: 12345
      slug: my-github-app
      name: My GitHub App
      private_key: |
        -----BEGIN RSA PRIVATE KEY-----
        ...your PEM key...
        -----END RSA PRIVATE KEY-----
      permissions:
        contents: read
        issues: write
      events: [push, pull_request]
      installations:
        - installation_id: 100
          account: my-org
          repository_selection: all
Sign JWTs with
{ iss: "<app_id>" }
using RS256 — the emulator verifies the signature.
yaml
github:
  apps:
    - app_id: 12345
      slug: my-github-app
      name: My GitHub App
      private_key: |
        -----BEGIN RSA PRIVATE KEY-----
        ...your PEM key...
        -----END RSA PRIVATE KEY-----
      permissions:
        contents: read
        issues: write
      events: [push, pull_request]
      installations:
        - installation_id: 100
          account: my-org
          repository_selection: all
使用RS256算法签署包含
{ iss: "<app_id>" }
的JWT——模拟器会验证签名。

Vercel Integrations

Vercel 集成

yaml
vercel:
  integrations:
    - client_id: $VERCEL_CLIENT_ID
      client_secret: $VERCEL_CLIENT_SECRET
      name: My Vercel App
      redirect_uris:
        - http://localhost:3000/api/auth/callback/vercel
yaml
vercel:
  integrations:
    - client_id: $VERCEL_CLIENT_ID
      client_secret: $VERCEL_CLIENT_SECRET
      name: My Vercel App
      redirect_uris:
        - http://localhost:3000/api/auth/callback/vercel

Real-World Test Patterns

实际测试场景示例

Testing a GitHub API Client

测试GitHub API客户端

typescript
import { createEmulator } from 'emulate'
import { Octokit } from '@octokit/rest'

describe('GitHub integration', () => {
  let emulator: Awaited<ReturnType<typeof createEmulator>>
  let octokit: Octokit

  beforeAll(async () => {
    emulator = await createEmulator({
      service: 'github',
      port: 4001,
      seed: {
        users: [{ login: 'testuser', name: 'Test User' }],
        repos: [{ owner: 'testuser', name: 'my-repo', auto_init: true }],
      },
    })

    octokit = new Octokit({
      baseUrl: emulator.url,
      auth: 'any-token',
    })
  })

  afterEach(() => emulator.reset())
  afterAll(() => emulator.close())

  it('creates and fetches an issue', async () => {
    const { data: issue } = await octokit.issues.create({
      owner: 'testuser',
      repo: 'my-repo',
      title: 'Test issue',
      body: 'This is a test',
    })

    expect(issue.number).toBe(1)
    expect(issue.state).toBe('open')

    const { data: fetched } = await octokit.issues.get({
      owner: 'testuser',
      repo: 'my-repo',
      issue_number: issue.number,
    })

    expect(fetched.title).toBe('Test issue')
  })
})
typescript
import { createEmulator } from 'emulate'
import { Octokit } from '@octokit/rest'

describe('GitHub integration', () => {
  let emulator: Awaited<ReturnType<typeof createEmulator>>
  let octokit: Octokit

  beforeAll(async () => {
    emulator = await createEmulator({
      service: 'github',
      port: 4001,
      seed: {
        users: [{ login: 'testuser', name: 'Test User' }],
        repos: [{ owner: 'testuser', name: 'my-repo', auto_init: true }],
      },
    })

    octokit = new Octokit({
      baseUrl: emulator.url,
      auth: 'any-token',
    })
  })

  afterEach(() => emulator.reset())
  afterAll(() => emulator.close())

  it('creates and fetches an issue', async () => {
    const { data: issue } = await octokit.issues.create({
      owner: 'testuser',
      repo: 'my-repo',
      title: 'Test issue',
      body: 'This is a test',
    })

    expect(issue.number).toBe(1)
    expect(issue.state).toBe('open')

    const { data: fetched } = await octokit.issues.get({
      owner: 'testuser',
      repo: 'my-repo',
      issue_number: issue.number,
    })

    expect(fetched.title).toBe('Test issue')
  })
})

Testing a Vercel Deployment Workflow

测试Vercel部署流程

typescript
import { createEmulator } from 'emulate'

describe('Vercel deployment', () => {
  let emulator: Awaited<ReturnType<typeof createEmulator>>

  beforeAll(async () => {
    emulator = await createEmulator({
      service: 'vercel',
      port: 4002,
      seed: {
        users: [{ username: 'dev', email: 'dev@example.com' }],
        projects: [{ name: 'my-app', framework: 'nextjs' }],
      },
    })
    process.env.VERCEL_API_URL = emulator.url
  })

  afterEach(() => emulator.reset())
  afterAll(() => emulator.close())

  it('creates a deployment and transitions to READY', async () => {
    const res = await fetch(`${emulator.url}/v13/deployments`, {
      method: 'POST',
      headers: {
        Authorization: 'Bearer any-token',
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ name: 'my-app', target: 'production' }),
    })

    const deployment = await res.json()
    expect(deployment.readyState).toBe('READY')
  })
})
typescript
import { createEmulator } from 'emulate'

describe('Vercel deployment', () => {
  let emulator: Awaited<ReturnType<typeof createEmulator>>

  beforeAll(async () => {
    emulator = await createEmulator({
      service: 'vercel',
      port: 4002,
      seed: {
        users: [{ username: 'dev', email: 'dev@example.com' }],
        projects: [{ name: 'my-app', framework: 'nextjs' }],
      },
    })
    process.env.VERCEL_API_URL = emulator.url
  })

  afterEach(() => emulator.reset())
  afterAll(() => emulator.close())

  it('creates a deployment and transitions to READY', async () => {
    const res = await fetch(`${emulator.url}/v13/deployments`, {
      method: 'POST',
      headers: {
        Authorization: 'Bearer any-token',
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ name: 'my-app', target: 'production' }),
    })

    const deployment = await res.json()
    expect(deployment.readyState).toBe('READY')
  })
})

Testing Multiple Services Together

同时测试多个服务

typescript
import { createEmulator, type Emulator } from 'emulate'

let github: Emulator
let vercel: Emulator
let google: Emulator

beforeAll(async () => {
  ;[github, vercel, google] = await Promise.all([
    createEmulator({ service: 'github', port: 4001 }),
    createEmulator({ service: 'vercel', port: 4002 }),
    createEmulator({ service: 'google', port: 4003 }),
  ])

  // Point your app's env vars at local emulators
  process.env.GITHUB_API_URL = github.url
  process.env.VERCEL_API_URL = vercel.url
  process.env.GOOGLE_API_URL = google.url
})

afterEach(() => {
  github.reset()
  vercel.reset()
  google.reset()
})

afterAll(() => Promise.all([github.close(), vercel.close(), google.close()]))
typescript
import { createEmulator, type Emulator } from 'emulate'

let github: Emulator
let vercel: Emulator
let google: Emulator

beforeAll(async () => {
  ;[github, vercel, google] = await Promise.all([
    createEmulator({ service: 'github', port: 4001 }),
    createEmulator({ service: 'vercel', port: 4002 }),
    createEmulator({ service: 'google', port: 4003 }),
  ])

  // 将应用的环境变量指向本地模拟器
  process.env.GITHUB_API_URL = github.url
  process.env.VERCEL_API_URL = vercel.url
  process.env.GOOGLE_API_URL = google.url
})

afterEach(() => {
  github.reset()
  vercel.reset()
  google.reset()
})

afterAll(() => Promise.all([github.close(), vercel.close(), google.close()]))

CI Configuration

CI 配置

GitHub Actions

GitHub Actions

yaml
undefined
yaml
undefined

.github/workflows/test.yml

.github/workflows/test.yml

jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 - run: npm ci - name: Run tests with emulated APIs run: npm test env: # Emulators start in vitest.setup.ts — no extra service needed NODE_ENV: test
undefined
jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 - run: npm ci - name: Run tests with emulated APIs run: npm test env: # 模拟器在vitest.setup.ts中启动——无需额外服务 NODE_ENV: test
undefined

Docker / No-Network Sandbox

Docker / 无网络沙箱

dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .

Tests start emulators programmatically — no outbound network needed

测试通过程序化方式启动模拟器——无需外部网络

RUN npm test
undefined
RUN npm test
undefined

Key API Endpoints Reference

核心API端点参考

GitHub Emulator

GitHub 模拟器

GET    /user                                    # authenticated user
GET    /repos/:owner/:repo                      # get repo
POST   /user/repos                              # create repo
POST   /repos/:owner/:repo/issues               # create issue
PATCH  /repos/:owner/:repo/issues/:number       # update issue
POST   /repos/:owner/:repo/pulls                # create PR
PUT    /repos/:owner/:repo/pulls/:number/merge  # merge PR
GET    /search/repositories                     # search repos
GET    /search/issues                           # search issues
GET    /user                                    # 已认证用户信息
GET    /repos/:owner/:repo                      # 获取仓库信息
POST   /user/repos                              # 创建仓库
POST   /repos/:owner/:repo/issues               # 创建Issue
PATCH  /repos/:owner/:repo/issues/:number       # 更新Issue
POST   /repos/:owner/:repo/pulls                # 创建PR
PUT    /repos/:owner/:repo/pulls/:number/merge  # 合并PR
GET    /search/repositories                     # 搜索仓库
GET    /search/issues                           # 搜索Issue

Vercel Emulator

Vercel 模拟器

GET    /v2/user                          # authenticated user
GET    /v2/teams                         # list teams
POST   /v11/projects                     # create project
GET    /v10/projects                     # list projects
POST   /v13/deployments                  # create deployment (auto → READY)
GET    /v13/deployments/:idOrUrl         # get deployment
POST   /v10/projects/:id/env             # create env vars
GET    /v10/projects/:id/env             # list env vars
GET    /v2/user                          # 已认证用户信息
GET    /v2/teams                         # 列出团队
POST   /v11/projects                     # 创建项目
GET    /v10/projects                     # 列出项目
POST   /v13/deployments                  # 创建部署(自动进入READY状态)
GET    /v13/deployments/:idOrUrl         # 获取部署信息
POST   /v10/projects/:id/env             # 创建环境变量
GET    /v10/projects/:id/env             # 列出环境变量

Troubleshooting

故障排查

Port already in use
bash
undefined
端口已被占用
bash
undefined

Use a different base port

使用其他基础端口

npx emulate --port 5000
npx emulate --port 5000

Or set via env

或通过环境变量设置

EMULATE_PORT=5000 npx emulate

**Tests interfering with each other**
```typescript
// Always call reset() in afterEach, not afterAll
afterEach(() => emulator.reset())
OAuth strict validation rejecting requests
  • If you configure
    oauth_apps
    or
    integrations
    , only matching
    client_id
    values are accepted
  • Remove the
    oauth_apps
    block to fall back to accept-any mode
Emulator not receiving requests from app code
typescript
// Make sure your app reads the URL from env at request time, not module load time
// ✅ Good
async function fetchUser() {
  return fetch(`${process.env.GITHUB_API_URL}/user`)
}

// ❌ Bad — captured before emulator starts
const API_URL = process.env.GITHUB_API_URL
GitHub App JWT auth failing
  • JWT must have
    { iss: "<app_id>" }
    as a string or number matching the configured
    app_id
  • Must be signed RS256 with the exact private key from config
  • The emulator verifies the signature — use a real RSA key pair in tests
EMULATE_PORT=5000 npx emulate

**测试相互干扰**
```typescript

务必在afterEach中调用reset(),而非afterAll

afterEach(() => emulator.reset())

**OAuth严格校验拒绝请求**
- 若配置了`oauth_apps`或`integrations`,仅会接受匹配的`client_id`
- 删除`oauth_apps`配置块即可回到接受任意client_id的模式

**模拟器未接收到应用代码的请求**
```typescript

确保应用在发起请求时从环境变量读取URL,而非在模块加载时捕获

// ✅ 正确方式 async function fetchUser() { return fetch(
${process.env.GITHUB_API_URL}/user
) }
// ❌ 错误方式——在模拟器启动前已捕获URL const API_URL = process.env.GITHUB_API_URL

**GitHub App JWT认证失败**
- JWT必须包含`{ iss: "<app_id>" }`,其值需与配置的`app_id`(字符串或数字类型)匹配
- 必须使用配置中的私钥通过RS256算法签署
- 模拟器会验证签名——测试中请使用真实的RSA密钥对