# rspack-plugin-mock

在 [Rspack](https://rspack.dev) and [Rsbuild](https://rsbuild.dev) 中注入 API mock 服务。

在 `rspack` 和 `rsbuild` 中实现与 [vite-plugin-mock-dev-server](https://github.com/pengzhanbo/vite-plugin-mock-dev-server) 完全一致的模拟开发服务。

<p align="center">
  <a href="https://www.npmjs.com/package/rspack-plugin-mock"><img alt="npm" src="https://img.shields.io/npm/v/rspack-plugin-mock?style=flat-square&colorA=564341&colorB=EDED91"></a>
  <img alt="node-current" src="https://img.shields.io/node/v/rspack-plugin-mock?style=flat-square&colorA=564341&colorB=EDED91">
  <img alt="npm peer dependency version" src="https://img.shields.io/npm/dependency-version/rspack-plugin-mock/peer/@rspack/core?style=flat-square&colorA=564341&colorB=EDED91&label=rspack">
  <img alt="npm peer dependency version" src="https://img.shields.io/npm/dependency-version/rspack-plugin-mock/peer/@rsbuild/core?style=flat-square&colorA=564341&colorB=EDED91&label=rsbuild">
  <img alt="npm" src="https://img.shields.io/npm/dm/rspack-plugin-mock?style=flat-square&colorA=564341&colorB=EDED91">
  <img alt="GitHub Workflow Status" src="https://img.shields.io/github/actions/workflow/status/pengzhanbo/rspack-plugin-mock/release.yaml?style=flat-square&colorA=564341&colorB=EDED91">
</p>

<p align="center">
<a href="./README.md">English</a> | <span>简体中文</span>
</p>

> [!IMPORTANT]
> 从插件 2.0 版本开始，仅支持 `rspack@2.x` 版本 和 `rsbuild@2.x` 版本.
>
> 如果您使用的是 `rspack@1.x` 或 `rsbuild@1.x` 版本，请使用 `rspack-plugin-mock@1.x` 版本.

## 特性

- ⚡️ 轻量，灵活，快速
- 🧲 非注入式，对客户端代码无侵入
- 💡 ESModule
- 🦾 Typescript
- 🔥 热更新
- 🏷 支持 `.[cm]?js`/ `.ts` / `json` / `json5` 编写 mock 数据
- 📦 自动加载 mock 文件
- 🎨 可选择你喜欢的任意用于生成mock数据库，如 `mockjs`，或者不使用其他库
- 📥 路径规则匹配，请求参数匹配
- ⚙️ 随意开启或关闭对某个接口的 mock配置
- 📀 支持多种响应体数据类型，包括 `text/json/buffer/stream`.
- ⚖️ rspack 中使用 `devServer.proxy` 配置, rsbuild 中使用 `server.proxy` 配置
- 🍕 支持在 mock 文件中使用 `define`配置
- ⚓️ 支持在 mock 文件中使用 `resolve.alias` 路径别名
- 📤 支持 multipart 类型，模拟文件上传
- 📥 支持模拟文件下载
- ⚜️ 支持模拟 `WebSocket` 和 `Server-Sent Events`
- 📝 支持 **请求录制** 和 **请求回放**
- 🗂 支持构建可独立部署的小型mock服务

## 安装

```sh
# npm
npm i -D rspack-plugin-mock
# yarn
yarn add rspack-plugin-mock
# pnp
pnpm add -D rspack-plugin-mock
```

## 使用

**In Rspack**

```ts
// rspack.config.js
import { MockServerPlugin } from 'rspack-plugin-mock'

export default {
  devServer: {
    // 插件将会读取 `proxy` 配置
    proxy: [
      { context: '/api', target: 'http://example.com' },
    ],
  },
  plugins: [
    new MockServerPlugin(/* pluginOptions */),
  ]
}
```

**In Rsbuild**

```ts
// rsbuild.config.ts
import { defineConfig } from '@rsbuild/core'
import { pluginMockServer } from 'rspack-plugin-mock/rsbuild'

export default defineConfig({
  server: {
    // 插件将会读取 `proxy` 配置
    proxy: {
      '/api': 'http://example.com',
    },
  },
  plugins: [
    pluginMockServer(/* pluginOptions */),
  ],
})
```

### 编写 mock 配置文件

插件默认会读取 项目根目录的 `mock` 目录：

`mock/**/*.mock.ts` :

```ts
import { defineMock } from 'rspack-plugin-mock/helper'

export default defineMock({
  url: '/api/test',
  body: { a: 1, b: 2 }
})
```

你可以使用 `.js, .mjs, .cjs, .ts, .json, .json5` 文件格式来编写 mock 配置。

## 方法

### MockServerPlugin(pluginOptions)

rspack mock 服务插件。

插件将会读取 `devServer.proxy` 配置，然后在 `@rspack/dev-server` 中注入中间件。

```js
import { MockServerPlugin } from 'rspack-plugin-mock'

export default {
  devServer: {
    // 插件将会读取 `proxy` 配置
    proxy: [
      { context: '/api', target: 'http://example.com' },
    ],
  },
  plugins: [
    new MockServerPlugin(/* 插件配置 */),
  ]
}
```

### pluginMockServer(pluginOptions)

rsbuild mock 服务插件. **仅适用于 `rsbuild`.**

```ts
// rsbuild.config.ts
import { defineConfig } from '@rsbuild/core'
import { pluginMockServer } from 'rspack-plugin-mock/rsbuild'

export default defineConfig({
  server: {
    // 插件将会读取 `proxy` 配置
    proxy: {
      '/api': 'http://example.com',
    },
  },
  plugins: [
    pluginMockServer(/* 插件配置 */),
  ],
})
```

### defineMock(options)

- **options:** [`MockOptions | MockOptions[]`](#mock-配置)

mock 配置类型帮助函数

```ts
import { defineMock } from 'rspack-plugin-mock/helper'

export default defineMock({
  url: '/api/test',
  body: { a: 1, b: 2 }
})
```

### createDefineMock(transformer)

- **transformer:** `(mock: MockOptions) => MockOptions`

返回一个自定义的 defineMock 函数，以支持对 mock 配置进行预处理。

```ts
import { createDefineMock } from 'rspack-plugin-mock/helper'

const definePostMock = createDefineMock((mock) => {
  mock.url = `/api/post/${mock.url}`
})

export default definePostMock({
  url: 'list', // => '/api/post/list'
  body: [{ title: '1' }, { title: '2' }],
})
```

### createSSEStream(req, res)

创建一个 `Server-sent events` 写入流，用于支持模拟 `EventSource`。

``` ts
import { createSSEStream, defineMock } from 'rspack-plugin-mock/helper'

export default defineMock({
  url: '/api/sse',
  response: (req, res) => {
    const sse = createSSEStream(req, res)
    sse.write({ event: 'message', data: { message: 'hello world' } })
    sse.end()
  }
})
```

## 插件配置

- **类型：** `boolean`
- **默认值：** `true`
- **详情：**

  是否开启 mock 服务。如果设置为 `false`，插件将不会生效。

### prefix

- **类型：** `string | string[]`
- **详情：**

  为 http mock 服务配置 路径匹配规则，任何请求路径以 prefix 开头的都将被拦截代理。
  如果 prefix 以 `^` 开头，将被识别为 `RegExp`。

### wsPrefix

- **类型：** `string | string[]`
- **详情：**

  为 websocket mock 服务配置 路径匹配规则， 任何请求路径以 wsPrefix 开头的 ws/wss请求，都将被代理拦截。
  如果 wsPrefix 以 `^` 开头，将被识别为 `RegExp`。

  请避免在 `devServer.proxy` / `server.proxy` 中出现 `wsPrefix` 配置中相同的规则，因为这可能会导致规则冲突。

### cwd

- **类型：** `string`
- **默认值：** `process.cwd()`
- **详情：**

  配置 `include` 和 `exclude` 的匹配上下文。

### dir

- **类型：** `string`
- **默认值：** `mock` (相对于 [`cwd`](#cwd))
- **详情：**

  配置 mock 包的输出目录，相对于 [`cwd`](#cwd)

### include

- **类型：** `string | string[]`
- **默认值：** `[**/*.mock.{js,ts,cjs,mjs,json,json5}']` (相对于 [`dir`](#dir))
- **详情：**

  glob 字符串匹配 mock 包含的文件。 查看 [picomatch](https://github.com/micromatch/picomatch#globbing-features)

### exclude

- **类型：** `string | string[]`
- **默认值：** `[]`  (相对于 [`dir`](#dir))
- **详情：**

  glob 字符串匹配 mock 排除的文件。 查看 [picomatch](https://github.com/micromatch/picomatch#globbing-features)

### log

- **类型：** `boolean | 'info' | 'warn' | 'error' | 'silent' | 'debug'`
- **默认值：** `info`
- **详情：**

  开启日志，或配置 日志级别

### reload

- **类型：** `boolean`
- **默认值：** `false`
- **详情：**

  mock资源热更新时，仅更新了数据内容，但是默认不重新刷新页面。
  当你希望每次修改mock文件都刷新页面时，可以打开此选项。

### activeScene

- **类型：** `string | string[]`
- **默认值：** `''`
- **详情：**

  当前激活的场景，用于过滤 mock。

  只有 [`scene`](#optionsscene) 与此有交集的 mock（或未配置 `scene` 的 mock）才会被考虑匹配。可通过 `X-Mock-Scene` 请求头按请求覆盖。

### cors

- **类型：** `boolean | CorsOptions`
- **默认值：** `true`
- **详情：**

  配置 [cors](https://github.com/expressjs/cors#configuration-options)

### formidableOptions

- **类型：** `FormidableOptions`
- **默认值：** `{ multiples: true }`
- **详情：**

  配置 [formidable](https://github.com/node-formidable/formidable#options)

### cookiesOptions

- **类型：** `CookiesOptions`
- **详情：**

  配置 [cookies](https://github.com/pillarjs/cookies#new-cookiesrequest-response--options)

### bodyParserOptions

- **类型：** `BodyParserOptions`
- **详情：**

  配置 [co-body](https://github.com/cojs/co-body#options)

## build

- **类型：** `boolean | ServerBuildOption`

  ```ts
  interface ServerBuildOption {
    /**
     * 服务启动端口
     * @default 8080
     */
    serverPort?: number
    /**
     * 构建输出目录，相对于 rspack/rsbuild 构建输出目录
     * @default 'mockServer'
     */
    dist?: string

    /**
     * 服务日志级别
     * @default 'error'
     */
    log?: LogLevel
  }
  ```

- **默认值：** `false`
- **详情：**

  当需要构建一个小型mock服务时，可配置此项。插件会在构建生产包时，额外生成一个可部署的node mock 服务包。

### record

- **类型：** `false | RecordOptions`
- **默认值：** `false`
- **详情：**

  是否开启请求录制功能。开启后，插件会记录所有请求数据，用于后续的请求回放。

  插件在 `proxy` 的基础上，记录被 `http-proxy` 代理的请求数据。
  在获得响应后，插件会将请求数据和响应数据记录到指定的目录中。

  ```ts
  interface RecordOptions {
    /**
     * 是否启用录制功能
     * - true: 启用，自动录制 proxy 响应
     * - false: 禁用（默认）
     * @default false
     */
    enabled?: boolean
    /**
     * 过滤要录制的请求
     * - 函数：自定义过滤函数，返回 true 表示录制
     * - 对象：包含/排除模式，支持 glob 或 path-to-regexp 模式
     * @example
     * ```ts
     * // Record all requests
     * filter: (req) => true
     * // Record requests using glob pattern
     * filter: { mode: 'glob', include: '/api/**' }
     * // Record requests using path-to-regexp pattern
     * filter: { mode: 'path-to-regexp', include: '/api/:id' }
     * ```
     */
    filter?: ((req: RecordedReq) => boolean) | {
      /**
       * 包含需要录制的请求链接
       *
       * glob 模式或 path-to-regexp 模式
       * (使用 mode 选项设置模式，默认为 glob)
       */
      include?: string | string[]
      /**
       * 排除不需要录制的请求链接
       *
       * glob 模式或 path-to-regexp 模式
       * (使用 mode 选项设置模式，默认为 glob)
       */
      exclude?: string | string[]
      /**
       * 包含/排除模式的匹配模式
       * - 'glob': glob 模式匹配（默认）
       * - 'path-to-regexp': path-to-regexp 模式匹配
       */
      mode: 'glob' | 'path-to-regexp'
    }
    /**
     * 录制数据存储目录
     * 相对于项目根目录
     * @default 'mock/.recordings'
     */
    dir?: string

    /**
     * 是否覆盖已有录制数据
     * - true: 相同请求覆盖旧数据（默认）
     * - false: 保留旧数据，不录制新数据
     * @default true
     */
    overwrite?: boolean
    /**
     * 录制数据过期时间（秒）
     * - 0: 永不过期（默认）
     * - 正数：指定秒数后过期
     * @default 0
     */
    expires?: number

    /**
     * 要录制的状态码
     * - 为空数组时记录所有状态码（默认）
     * - 指定一个或多个状态码进行过滤
     * @default []
     */
    status?: number | number[]

    /**
     * 是否在录制目录中添加 .gitignore
     * - true: 添加（默认）
     * - false: 不添加
     * @default true
     */
    gitignore?: boolean
  }
  ```

### replay

- **类型：** `boolean`
- **默认值：** `false`
- **详情：**

  是否开启请求回放功能。开启后，插件会根据记录的请求数据，模拟响应。

### priority

- **类型：** `MockMatchPriority`
- **详情：**

  自定义 路径匹配规则优先级。[查看更多](#自定义匹配优先级)

  **默认值：** `undefined`

## Mock 配置

**http mock**

```ts
import { defineMock } from 'rspack-plugin-mock/helper'
export default defineMock({
  url: '/api/test',
  body: { message: 'hello world' }
})
```

**websocket mock**

```ts
import { defineMock } from 'rspack-plugin-mock/helper'

export default defineMock({
  url: '/socket.io',
  ws: true,
  setup(wss) {
    wss.on('connection', (ws, req) => {
      console.log('connected')
    })
  }
})
```

### options.url

- **类型：** `string`
- **详情：**

  需要进行 mock 的接口地址, 由 [path-to-regexp](https://github.com/pillarjs/path-to-regexp) 提供路径匹配支持。

### options.enabled

- **类型：** `boolean`
- **默认值：** `true`
- **详情：**

  是否启动对该接口的mock，在多数场景下，我们仅需要对部分接口进行 mock，
  而不是对所有配置了mock的请求进行全量mock，所以是否能够配置是否启用很重要

### options.scene

- **类型：** `string | string[]`
- **默认值：** `''`
- **详情：**

  该 mock 的场景标识。

  未配置时，该 mock 为全场景通用，不受 [activeScene](#activescene) 限制。
  配置后，只有 `scene` 中任意一项与 [activeScene](#activescene) 中任意一项匹配时，该 mock 才会激活。

### options.method

- **类型：** `Method | Method[]`

  ```ts
  type Method = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'OPTIONS' | 'HEAD' | 'PATCH'
  ```

- **默认值：** `['GET', 'POST']`
- **详情：**

  该接口允许的 请求方法，默认同时支持 GET 和 POST

### options.type

- **类型：** `'text' | 'json' | 'buffer' | string`
- **详情：**

  响应体数据类型。 还支持 [mime-db](https://github.com/jshttp/mime-db) 中的包含的类型。

  当响应体返回的是一个文件，而你不确定应该使用哪个类型时，可以将文件名作为值传入，
  插件内部会根据文件名后缀查找匹配的`content-type`。

### options.headers

- **类型：** `object | (request: MockRequest) => object | Promise<object>`
- **默认值：** `{ 'Content-Type': 'application/json' }`
- **详情：**

  配置响应体 headers

### options.status

- **类型：** `number`
- **默认值：** `200`
- **详情：**

  配置 响应头状态码

### options.statusText

- **类型：** `string`
- **默认值：** `"OK"`
- **详情：**

  配置响应头状态文本

### options.delay

- **类型：** `number | [number, number]`
- **默认值：** `0`
- **详情：**

  配置响应延迟时间, 如果传入的是一个数组，则代表延迟时间的范围。

  单位： `ms`

### options.body

- **类型：** `Body | (request: MockRequest) => Body | Promise<Body>`

  ```ts
  type Body = string | object | Buffer | Readable
  ```

- **详情：**

  配置响应体数据内容 `body` 优先级高于 `response`.

### options.response

- **类型：** `(req: MockRequest, res: MockResponse, next: (err?: any) => void) => void | Promise<void>`
- **详情：**

  如果需要设置复杂的响应内容，可以使用 response 方法，
  该方法是一个 middleware，你可以在这里拿到 http 请求的 req、res等信息，
  然后通过 `res.write() | res.end()` 返回响应数据， 否则需要执行 `next()` 方法。
  在 `req` 中，还可以拿到 `query、params、body, refererQuery` 等已解析的请求信息。

### options.cookies

- **类型：** `CookiesOptions | (request: MockRequest) => CookiesOptions | Promise<CookiesOptions>`

  ```ts
  type CookiesOptions = Record<string, CookieValue>

  type CookieValue = string | [string, SetCookie]
  ```

- **详情：**

  配置响应体 cookies

### options.validator

- **类型：** `Validator | (request: MockRequest) => boolean`

  ```ts
  interface Validator {
    /**
     * 请求地址中位于 `?` 后面的 queryString，已解析为 json
     */
    query: Record<string, any>
    /**
     * 请求 referer 中位于 `?` 后面的 queryString
     */
    refererQuery: Record<string, any>
    /**
     * 请求体中 body 数据
     */
    body: Record<string, any>
    /**
     * 请求地址中，`/api/id/:id` 解析后的 params 参数
     */
    params: Record<string, any>
    /**
     * 请求体中 headers
     */
    headers: Headers
  }
  ```

- **详情：**

  请求验证器

  有时候，一个相同的API请求，需要根据不同的请求参数，来决定返回数据，
  但全部都在单个 mock中的 body或者 response 中写，内容会很庞杂，不好管理，
  验证器的功能，允许你同时配置多条相同url的mock，通过验证器来判断使哪个mock生效。

### options.ws

- **类型：** `boolean`
- **默认值：** `false`
- **详情：**

  配置是否开启 WebSocket Mock

### options.setup

- **类型：** `(wss: WebSocketServer, ctx: WebSocketSetupContext) => void`
- **详情：**

  配置 Websocket Server

```ts
interface WebSocketSetupContext {
  /**
   * 当你在定义 WSS 时，可能会执行一些自动任务或循环任务，
   * 但是当热更新时，插件内部会重新执行 setup() ，
   * 这可能导致出现 重复注册监听事件 和 循环任务如 `setTimeout` 等。
   * 通过 `onCleanup()` 可以来清除这些自动任务或循环任务。
   */
  onCleanup: (cleanup: () => void) => void
}
```

### options.error

- **类型：** `MockErrorConfig | undefined`
- **详情：**

  配置错误模拟，包括错误概率、错误状态码、错误状态文本、以及自定义错误响应体。

```ts
interface MockErrorConfig {
  /**
   * 错误概率（0-1），默认 0.5
   * @default 0.5
   */
  probability?: number
  /**
   * 错误状态码，默认 500
   * @default 500
   */
  status?: number
  /**
   * 错误状态文本
   */
  statusText?: string
  /**
   * 自定义错误响应体，适用于 status 为 200，但响应体需要模拟错误场景
   * @example
   * { code: 500, msg: 'Internal Server Error', result: null }
   */
  body?: ResponseBody | ResponseBodyFn
}
```

### Request/Response 增强

当你配置 `headers`, `body`, and `response` 的函数形式时, 插件在参数 `request` 和 `response` 添加了新的内容用于帮助获取必要的数据.

**Request:**

`request`的原始数据类型是[`Connect.IncomingMessage`](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/connect/index.d.ts). 插件在此基础上，增加了 `query`, `params`, `body`, `refererQuery`，以及 `getCookie(name)` 方法用于获取cookie信息。

```ts
type Request = Connect.IncomingMessage & {
  query: object
  params: object
  body: any
  refererQuery: object
  getCookie: (name: string, option?: Cookies.GetOption) => string | undefined
}
```

**Response:**

`response` 的原始数据类型是`http.ServerResponse<http.IncomingMessage>`. 插件在此基础上增加了 `setCookie(name, value)` 方法用于设置cookie

```ts
type Response = http.ServerResponse<http.IncomingMessage> & {
  setCookie: (
    name: string,
    value?: string | null,
    option?: Cookies.SetOption,
  ) => void
}
```

> **注意：**
>
> 如果使用 json/json5 编写 mock文件，则不支持使用 `response` 方法，以及不支持使用其他字段的函数形式。

## 共享 Mock 数据

由于每个mock文件都是作为独立的入口进行编译，其依赖的本地文件也编译在内，
且每个mock文件拥有独立的作用域，这使得即使多个 mock文件共同依赖某一个`data.ts`文件，也无法共享数据。
某个 `mock` 文件对 `data.ts` 中的数据进行修改，其它`mock`文件不会获取到修改后的数据。

为此，插件提供了一个 `defineMockData` 函数，用于在 `mock` 文件中使用 `data.ts` 作为共享数据源。

```ts
type defineMockData<T> = (
  key: string, // 数据唯一标识符
  initialData: T, // 初始化数据
  options?: {
    persistOnHMR?: boolean // 是否在热更新时保持数据状态
  } // 可选配置
) => [getter, setter] & { value: T }
```

### 用法

`data.ts`

```ts
import { defineMockData } from 'rspack-plugin-mock/helper'

export default defineMockData('posts', [
  { id: '1', title: 'title1', content: 'content1' },
  { id: '2', title: 'title2', content: 'content2' },
])
```

`*.mock.ts`

```ts
import { defineMock } from 'rspack-plugin-mock/helper'
import posts from './data'

export default defineMock([
  {
    url: '/api/posts',
    body: () => posts.value
  },
  {
    url: '/api/posts/delete/:id',
    body: (params) => {
      const id = params.id
      posts.value = posts.value.filter(post => post.id !== id)
      return { success: true }
    }
  }
])
```

> **注意：**
>
> `defineMockData` 仅是基于 `memory` 提供的共享数据支持，
> 如果需要做 mock 数据持久化，建议使用 `nosql`， 如 `lowdb` 或 `level` 等。

## 自定义匹配优先级

> 自定义规则仅影响包含动态参数的链接，如： `/api/user/:id`

插件内置的路径匹配规则优先级，已经能够满足大部分需求，但如果你需要更加灵活的自定义匹配规则优先级，
可以使用 `priority` 参数。

示例：

```ts
import { MockServerPlugin } from 'rspack-plugin-mock'

export default {
  plugins: [
    new MockServerPlugin({
      priority: {
        // 匹配规则优先级, 全局生效。声明在该选项中的规则将优先于默认规则生效。
        // 规则在数组越靠前的位置，优先级越高。
        global: ['/api/:a/b/c', '/api/a/:b/c', '/api/a/b/:c'],
        // 对于一些特殊情况，需要调整部分规则的优先级，可以使用此选项。
        // 比如一个请求同时命中了规则 A 和 B，且 A 比 B 优先级高， 但期望规则 B 生效时。
        special: {
          // 当请求同时命中 [key] 和 rules 中的任意一个时，优先匹配 [key] 。
          // when 用于进一步约束具体是哪些请求需要调整优先级。
          '/api/:a/:b/c': {
            rules: ['/api/a/:b/:c', '/api/a/b/:c'],
            when: ['/api/a/b/c']
          },
          // 如果不需要 when, 则表示命中规则的请求都需要调整优先级。
          // 可以简写为 [key]: [...rules]
          '/api/:a/b': ['/api/a/:b'],
        }
      }
    })
  ]
}
```

> **注意：**
>
> `priority` 虽然可以调整优先级，但大多数时候，你都没有必要这么做。
> 对于一些特殊情况的请求，可以使用 静态规则来替代 `priority`，静态规则总是拥有最高优先级。

## Example

`mock/**/*.mock.{ts,js,mjs,cjs,json,json5}`

查看更多示例： [example](/example/)

<details>
<summary>命中 <code>/api/test</code> 请求，并返回一个 数据为空的响应体内容</summary>

```ts
export default defineMock({
  url: '/api/test',
})
```

</details>
<details>
<summary>命中 <code>/api/test</code> 请求，并返回一个固定内容数据</summary>

```ts
export default defineMock({
  url: '/api/test',
  body: { a: 1 },
})
```

```ts
export default defineMock({
  url: '/api/test',
  body: () => ({ a: 1 })
})
```

</details>

<details>
<summary>限定只允许 <code>GET</code> 请求</summary>

```ts
export default defineMock({
  url: '/api/test',
  method: 'GET'
})
```

</details>

<details>
<summary>在返回的响应头中，添加自定义 header 和 cookie</summary>

```ts
export default defineMock({
  url: '/api/test',
  headers: { 'X-Custom': '12345678' },
  cookies: { 'my-cookie': '123456789' },
})
```

```ts
export default defineMock({
  url: '/api/test',
  headers({ query, body, params, headers }) {
    return { 'X-Custom': query.custom }
  },
  cookies() {
    return { 'my-cookie': '123456789' }
  }
})
```

</details>

<details>
<summary>定义多个相同url请求mock，并使用验证器匹配生效规则</summary>

```ts
export default defineMock([
  // 命中 /api/test?a=1
  {
    url: '/api/test',
    validator: {
      query: { a: 1 },
    },
    body: { message: 'query.a === 1' },
  },
  // 命中 /api/test?a=2
  {
    url: '/api/test',
    validator: {
      query: { a: 2 },
    },
    body: { message: 'query.a === 2' },
  },
  {
    // `?a=3` 将会解析到 `validator.query`
    url: '/api/test?a=3',
    body: { message: 'query.a == 3' },
  },
  // 命中 POST /api/test 请求，且 请求体中，字段 a 为数组，且数组包含值为 1， 2 的项
  {
    url: '/api/test',
    method: ['POST'],
    validator: { body: { a: [1, 2] } }
  }
])
```

</details>

<details>
<summary>延迟接口响应</summary>

```ts
export default defineMock({
  url: '/api/test',
  delay: 6000, // 延迟 6秒
})
```

</details>

<details>
<summary>使接口请求失败</summary>

```ts
export default defineMock({
  url: '/api/test',
  status: 502,
  statusText: 'Bad Gateway'
})
```

</details>

<details>
<summary>动态路由匹配</summary>

```ts
export default defineMock({
  url: '/api/user/:userId',
  body({ params }) {
    return { userId: params.userId }
  }
})
```

路由中的 `userId`将会解析到 `request.params` 对象中.

</details>

<details>
<summary>使用 buffer 响应数据</summary>

```ts
import { Buffer } from 'node:buffer'

// 由于 type 默认值是 json，虽然在传输过程中body使用buffer，
// 但是 content-type 还是为 json
export default defineMock({
  url: 'api/buffer',
  body: Buffer.from(JSON.stringify({ a: 1 }))
})
```

```ts
// 当 type 为 buffer 时，content-type 为 application/octet-stream，
// body 传入的数据会被转为 buffer
export default defineMock({
  url: 'api/buffer',
  type: 'buffer',
  // 内部使用 Buffer.from(body) 进行转换
  body: { a: 1 }
})
```

</details>

<details>
<summary>响应文件类型</summary>

模拟文件下载，传入文件读取流

```ts
import { createReadStream } from 'node:fs'

export default defineMock({
  url: '/api/download',
  // 当你不确定类型，可传入文件名由插件内部进行解析
  type: 'my-app.dmg',
  body: () => createReadStream('./my-app.dmg')
})
```

```html
<a href="/api/download" download="my-app.dmg">下载文件</a>
```

</details>

<details>
<summary>使用 <code>mockjs</code> 生成响应数据</summary>

```ts
import Mock from 'mockjs'

export default defineMock({
  url: '/api/test',
  body: Mock.mock({
    'list|1-10': [{
      'id|+1': 1
    }]
  })
})
```

请先安装 `mockjs`

</details>

<details>
<summary>使用 <code>response</code> 自定义响应</summary>

```ts
export default defineMock({
  url: '/api/test',
  response(req, res, next) {
    const { query, body, params, headers } = req
    console.log(query, body, params, headers)

    res.status = 200
    res.setHeader('Content-Type', 'application/json')
    res.end(JSON.stringify({
      query,
      body,
      params,
    }))
  }
})
```

</details>

<details>
<summary>使用 json / json5</summary>

```json
{
  "url": "/api/test",
  "body": {
    "a": 1
  }
}
```

</details>

<details>
<summary>multipart, 文件上传</summary>

通过 [`formidable`](https://www.npmjs.com/package/formidable#readme) 支持。

``` html
<form action="/api/upload" method="post" enctype="multipart/form-data">
  <p>
    <span>file: </span>
    <input type="file" name="files" multiple="multiple">
  </p>
  <p>
    <span>name:</span>
    <input type="text" name="name" value="mark">
  </p>
  <p>
    <input type="submit" value="submit">
  </p>
</form>
```

fields `files` 映射为 `formidable.File` 类型。

``` ts
export default defineMock({
  url: '/api/upload',
  method: 'POST',
  body(req) {
    const body = req.body
    return {
      name: body.name,
      files: body.files.map((file: any) => file.originalFilename),
    }
  },
})
```

</details>

<details>
<summary>Graphql</summary>

```ts
import { buildSchema, graphql } from 'graphql'

const schema = buildSchema(`
type Query {
  hello: String
}
`)
const rootValue = { hello: () => 'Hello world!' }

export default defineMock({
  url: '/api/graphql',
  method: 'POST',
  body: async (request) => {
    const source = request.body.source
    const { data } = await graphql({ schema, rootValue, source })
    return data
  },
})
```

```ts
fetch('/api/graphql', {
  method: 'POST',
  body: JSON.stringify({ source: '{ hello }' })
})
```

</details>

<details>
<summary>WebSocket Mock</summary>

```ts
// ws.mock.ts
export default defineMock({
  url: '/socket.io',
  ws: true,
  setup(wss, { onCleanup }) {
    const wsMap = new Map()
    wss.on('connection', (ws, req) => {
      const token = req.getCookie('token')
      wsMap.set(token, ws)
      ws.on('message', (raw) => {
        const data = JSON.parse(String(raw))
        if (data.type === 'ping')
          return
        // Broadcast
        for (const [_token, _ws] of wsMap.entires()) {
          if (_token !== token)
            _ws.send(raw)
        }
      })
    })
    wss.on('error', (err) => {
      console.error(err)
    })
    onCleanup(() => wsMap.clear())
  }
})
```

```ts
// app.ts
const ws = new WebSocket('ws://localhost:5173/socket.io')
ws.addEventListener('open', () => {
  setInterval(() => {
    // heartbeat
    ws.send(JSON.stringify({ type: 'ping' }))
  }, 1000)
}, { once: true })
ws.addEventListener('message', (raw) => {
  console.log(raw)
})
```

</details>

<details>
<summary>EventSource Mock</summary>

```ts
// sse.mock.ts
import { createSSEStream, defineMock } from 'rspack-plugin-mock/helper'

export default defineMock({
  url: '/api/sse',
  response(req, res) {
    const sse = createSSEStream(req, res)
    let count = 0
    const timer = setInterval(() => {
      sse.write({
        event: 'count',
        data: { count: ++count },
      })
      if (count >= 10) {
        sse.end()
        clearInterval(timer)
      }
    }, 1000)
  },
})
```

```ts
// app.js
const es = new EventSource('/api/sse')

es.addEventListener('count', (e) => {
  console.log(e.data)
})
```

</details>

## 独立部署的小型mock服务

在一些场景中，可能会需要使用mock服务提供的数据支持，用于展示，但可能项目已完成打包构建部署，已脱离 `rspack/rsbuild` 和本插件提供的 mock服务支持。由于本插件在设计之初，支持在mock文件中引入各种 `node` 模块，所以不能将 mock文件打包内联到客户端构建代码中。

为了能够满足这类场景，插件提供了在 `production build` 时，也构建一个可独立部署的小型mock服务应用，可以将这个应用部署到相关的环境，后通过其他http服务器如nginx做代理转发到实际端口实现mock支持。

构建默认输出到 `mockServer` 目录中，并生成如下文件：

```sh
./mockServer
├── index.js
├── mock-data.js
└── package.json
```

在该目录下，执行 `npm install` 安装依赖后，执行 `npm start` 即可启动 mock server。
默认端口为 `8080`。
可通过 `localhost:8080/` 访问相关的 `mock` 接口。

## Links

- [rspack](https://rspack.dev)
- [rsbuild](https://rsbuild.dev)
- [vite-plugin-mock-dev-server](https://github.com/pengzhanbo/vite-plugin-mock-dev-server)

## License

rspack-plugin-mock is licensed under the [MIT License](./LICENSE)
