vite-plugin-mock-dev-server
Version:
Vite Plugin for API mock dev server.
573 lines (571 loc) • 18.2 kB
TypeScript
import { Readable } from "node:stream";
import { CorsOptions } from "cors";
import { Buffer } from "node:buffer";
import Cookies from "cookies";
import { Options } from "co-body";
import formidable from "formidable";
import { WebSocketServer } from "ws";
import http from "node:http";
import { Connect } from "vite";
//#region src/types.d.ts
/**
* Configure plugin
*
* 插件配置项
*/
interface MockServerPluginOptions {
/**
* To configure the path matching rules for http mock services,
* any request path starting with prefix will be intercepted and proxied.
* If the prefix starts with `^`, it will be recognized as a `RegExp`.
*
* 为 http mock 服务配置 路径匹配规则,任何请求路径以 prefix 开头的都将被拦截代理。
* 如果 prefix 以 `^` 开头,将被识别为 `RegExp`。
* @default []
* @example ['^/api']
*/
prefix?: string | string[];
/**
* Configure path matching rules for WebSocket mock service.
* Any ws/wss requests with a request path starting with wsPrefix
* will be intercepted by the proxy.
* If wsPrefix starts with `^`, it will be recognized as a `RegExp`.
*
* 为 websocket mock 服务配置 路径匹配规则, 任何请求路径以 wsPrefix 开头的 ws/wss请求,
* 都将被代理拦截。
* 如果 wsPrefix 以 `^` 开头,将被识别为 `RegExp`。
* @default []
* @example ['/socket.io']
*/
wsPrefix?: string | string[];
/**
* Configure the matching context for `include` and `exclude`.
*
* 配置 `include` 和 `exclude` 的匹配上下文
*
* @default process.cwd()
*/
cwd?: string;
/**
* glob string matching mock includes files
*
* glob 字符串匹配 mock 包含的文件
* @see [picomatch](https://github.com/micromatch/picomatch#globbing-features)
* @default []
*/
include?: string | string[];
/**
* glob string matching mock excluded files
*
* glob 字符串匹配 mock 排除的文件
* @see [picomatch](https://github.com/micromatch/picomatch#globbing-features)
*/
exclude?: string | string[];
/**
* Enable log and configure log level
*
* 开启日志,或配置 日志级别
* @default 'info'
*/
log?: boolean | LogLevel;
/**
* When the mock resource is hot updated, only the data content is updated,
* but the page is not refreshed by default.
* If you want to refresh the page every time you modify a mock file,
* you can open this option.
*
* mock资源热更新时,仅更新了数据内容,但是默认不重新刷新页面。
* 当你希望每次修改mock文件都刷新页面时,可以打开此选项。
* @default false
*/
reload?: boolean;
/**
* Configure to `cors`
*
* 配置 `cors`
* @default true
* @see [cors](https://github.com/expressjs/cors#configuration-options)
*/
cors?: boolean | CorsOptions;
/**
* formidable options
* @see [formidable](https://github.com/node-formidable/formidable#options)
*/
formidableOptions?: formidable.Options;
/**
* cookies options
* @see [cookies](https://github.com/pillarjs/cookies#new-cookiesrequest-response--options)
*/
cookiesOptions?: Cookies.Option;
/**
* Configure to `co-body`
*
* 配置 `co-body`
*
* @see [co-body](https://github.com/cojs/co-body#options)
*/
bodyParserOptions?: BodyParserOptions;
/**
* When you need to build a small mock service, you can configure this option.
*
* 当需要构建一个小型mock服务时,可配置此项
* @default false
*/
build?: boolean | ServerBuildOption;
/**
* Priority sorting for path matching rules is valid only for rules containing dynamic parameters.
* In most cases, the default sorting rules can meet the needs.
* However, in some cases where custom sorting rules are required, this option can be used.
*
* 路径匹配规则优先级排序,仅对包含动态参数的规则有效。
* 大部分情况下默认的排序规则都可以满足需求。
* 但有些情况下,需要自定义排序规则时,可以使用此选项。
*
* @example
* ```ts
* export default {
* priority: {
* global: ['/api/:a/b/c', '/api/a/:b/c', '/api/a/b/:c'],
* special: {
* '/api/:a/:b/c': {
* rules: ['/api/a/:b/:c', '/api/a/b/:c'],
* when: ['/api/a/b/c']
* }
* }
* }
* }
* ```
*/
priority?: MockMatchPriority;
}
interface MockMatchPriority {
/**
* The priority of matching rules is global.
* The rules declared in this option will take priority over the default rules.
* The higher the position of the rule in the array, the higher the priority.
*
* Do not declare general rules in this option, such as /api/(.*),
* as it will prevent subsequent rules from taking effect.
* Unless you are clear about the priority of the rules,
* most of the time you do not need to configure this option.
*
* 匹配规则优先级, 全局生效。
* 声明在该选项中的规则将优先于默认规则生效。
* 规则在数组越靠前的位置,优先级越高。
*
* 不要在此选项中声明通用性的规则,比如 `/api/(.*)`,这将导致后续的规则无法生效。
* 除非你明确知道规则的优先级,否则大多数情况下都不需要配置该选项。
* @default []
*/
global?: string[];
/**
* For some special cases where the priority of certain rules needs to be adjusted,
* this option can be used. For example, when a request matches both Rule A and Rule B,
* and Rule A has a higher priority than Rule B, but it is desired for Rule B to take effect.
*
* 对于一些特殊情况,需要调整部分规则的优先级,可以使用此选项。
* 比如一个请求同时命中了规则 A 和 B,且 A 比 B 优先级高, 但期望规则 B 生效时。
*
* @example
* ```ts
* {
* special: {
* // /api/a/:b/c 优先级将提升到 /api/a/b/:c 前面
* // The /api/a/:b/c priority is promoted to /api/a/b/:c
* '/api/a/:b/c': ['/api/a/b/:c'],
* // 仅在请求满足 /api/a/b/c 时生效
* // Only when the request satisfies /api/a/b/c
* '/api/:a/b/c': {
* rules: ['/api/a/:b/c'],
* when: ['/api/a/b/c']
* }
* }
* }
* ```
*/
special?: MockMatchSpecialPriority;
}
interface MockMatchSpecialPriority {
/**
* When both A and B or C match, and B or C is at the top of the sort order,
* insert A into the top position.The `when` option is used to further constrain
* the priority adjustment to be effective only for certain requests.
*
* 当 A 与 B或 C 同时满足匹配,`B` 或 `C` 在排序首位时,将A插入到首位。
* when 选项用于进一步约束该优先级调整仅针对哪些请求有效。
*
* @example
* ```ts
* {
* A: ['B', 'C'],
* A: { rules: ['B', 'C'], when: ['/api/a/b/c'] }
* }
* ```
*/
[key: string]: string[] | {
rules: string[];
when: string[];
};
}
type BodyParserOptions = Options & {
jsonLimit?: string | number;
formLimit?: string | number;
textLimit?: string | number;
};
interface ServerBuildOption {
/**
* Service startup port
*
* 服务启动端口
* @default 8080
*/
serverPort?: number;
/**
* Service application output directory
*
* 服务应用输出目录
* @default 'dist/mockServer'
*/
dist?: string;
/**
* Service application log level
*
* 服务应用日志级别
* @default 'error'
*/
log?: LogLevel;
}
type Method = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "HEAD" | "TRACE" | "OPTIONS";
type Headers = http.IncomingHttpHeaders;
type ResponseBody = Record<string, any> | any[] | string | number | Readable | Buffer | null;
/**
* 扩展 request,添加额外的属性和方法
*/
interface ExtraRequest {
/**
* The query string located after `?` in the request address has been parsed into JSON.
*
* 请求地址中位于 `?` 后面的 queryString,已解析为 json
*/
query: Record<string, any>;
/**
* The queryString located after `?` in the referer request has been parsed as JSON.
*
* 请求 referer 中位于 `?` 后面的 queryString,已解析为 json
*/
refererQuery: Record<string, any>;
/**
* Body data in the request
*
* 请求体中 body 数据
*/
body: Record<string, any>;
/**
* The params parameter parsed from the `/api/id/:id` in the request address.
*
* 请求地址中,`/api/id/:id` 解析后的 params 参数
*/
params: Record<string, any>;
/**
* headers data in the request
* 请求体中 headers
*/
headers: Headers;
/**
* Get the cookie carried in the request.
*
* 获取 请求中携带的 cookie
* @see [cookies](https://github.com/pillarjs/cookies#cookiesgetname--options)
*/
getCookie: (name: string, option?: Cookies.GetOption) => string | undefined;
}
type MockRequest = Connect.IncomingMessage & ExtraRequest;
type MockResponse = http.ServerResponse<http.IncomingMessage> & {
/**
* Set cookie in response
*
* 向请求响应中设置 cookie
* @see [cookies](https://github.com/pillarjs/cookies#cookiessetname--values--options)
*/
setCookie: (name: string, value?: string | null, option?: Cookies.SetOption) => void;
};
type ResponseBodyFn = (request: MockRequest) => ResponseBody | Promise<ResponseBody>;
type ResponseHeaderFn = (request: MockRequest) => Headers | Promise<Headers>;
type CookieValue = string | [string, Cookies.SetOption];
type ResponseCookies = Record<string, CookieValue>;
type ResponseCookiesFn = (request: MockRequest) => ResponseCookies | Promise<ResponseCookies>;
interface MockBaseItem {
/**
* The interface address that needs to be mocked,
* supported by `path-to-regexp` for path matching.
*
* 需要进行 mock 的接口地址, 由 `path-to-regexp` 提供路径匹配支持
* @see [path-to-regexp](https://github.com/pillarjs/path-to-regexp)
* @example
* ```txt
* /api/login
* /api/post/:id
* /api/post/:id
* /api/anything/(.*)
* ```
*/
url: string;
/**
* Enable WebSocket interface simulation
*
* 开启 websocket 接口模拟
*
* @default false
*/
ws?: boolean;
/**
* Whether to enable mock for this interface.
* In most scenarios, we only need to mock some interfaces instead of all requests that
* have been configured with mock.
* Therefore, it is important to be able to configure whether to enable it or not.
*
* 是否启动对该接口的mock,在多数场景下,我们仅需要对部分接口进行 mock,
* 而不是对所有配置了mock的请求进行全量mock,所以是否能够配置是否启用很重要
* @default true
*/
enabled?: boolean;
/**
* Enable log and configure log level
*
* 开启日志,或配置 日志级别
* @default 'info'
*/
log?: boolean | LogLevel;
}
interface MockHttpItem extends MockBaseItem {
/**
* The interface allows request methods, and by default allows both GET and POST.
*
* 该接口允许的 请求方法,默认同时支持 GET 和 POST
* @default ['POST','GET']
*/
method?: Method | Method[];
/**
* Configure the response body headers
*
* 配置响应体 headers
* @default
* ```json
* { "Content-Type": "application/json" }
* ```
*/
headers?: Headers | ResponseHeaderFn;
/**
* Configure Response Header Status Code
*
* 配置 响应头状态码
* @default 200
*/
status?: number;
/**
* Configure response header status text
*
* 配置响应头状态文本
* @default 'OK'
*/
statusText?: string;
/**
* Configure response delay time,
* If an array is passed in, it represents the range of delay time.
* unit: `ms`
*
* 配置响应延迟时间, 如果传入的是一个数组,则代表延迟时间的范围
* 单位: `ms`
* @default 0
*/
delay?: number | [number, number];
/**
* Configure response body cookies
*
* 设置响应体 cookies
* @example
* ```ts
* export default {
* cookies: {
* 'token1': '1234567',
* 'token2': ['1234567', { path: '/' }],
* },
* }
* ```
* @example
* ```ts
* export default {
* cookies: function (request) {
* return {
* 'token1': '1234567',
* 'token2': ['1234567', { path: '/' }],
* }
* },
* }
* ```
*/
cookies?: ResponseCookies | ResponseCookiesFn;
/**
* Response body data type, optional values include `text, json, buffer`.
*
* And also support types included in `mime-db`.
* When the response body returns a file and you are not sure which type to use,
* you can pass the file name as the value. The plugin will internally search for matching
* `content-type` based on the file name suffix.
*
* However, if it is a TypeScript file such as `a.ts`, it may not be correctly matched
* as a JavaScript script. You need to modify `a.ts` to `a.js` as the value passed
* in order to recognize it correctly.
*
* 响应体数据类型, 可选值包括 `text, json, buffer`,
*
* 还支持`mime-db`中的包含的类型。
* 当响应体返回的是一个文件,而你不确定应该使用哪个类型时,可以将文件名作为值传入,
* 插件内部会根据文件名后缀查找匹配的`content-type`。
*
* 但如果是 `typescript`文件如 `a.ts`,可能不会被正确匹配为 `javascript`脚本,
* 你需要将 `a.ts` 修改为 `a.js`作为值传入才能正确识别。
* @see [mime-db](https://github.com/jshttp/mime-db)
* @default 'json'
* @example
* ```txt
* json
* buffer
* my-app.dmg
* music.mp4
* ```
*/
type?: "text" | "json" | "buffer" | string;
/**
* Configure response body data content
*
* 配置响应体数据内容
* @default ''
* @example
* ```ts
* export default {
* body: { a: 1 },
* }
* ```
* @example
* ```ts
* export default {
* body: function(request) {
* return { a: 1, query: request.query }
* },
* }
* ```
*/
body?: ResponseBody | ResponseBodyFn;
/**
* If you need to set complex response content, you can use the response method,
* which is a middleware. Here, you can get information such as req
* and res of the http request,
* and then return response data through res.write() | res.end().
* Otherwise, you need to execute next() method.
* In `req`, you can also get parsed request information such as
* `query`, `params`, `body` and `refererQuery`.
*
* 如果需要设置复杂的响应内容,可以使用 response 方法,
* 该方法是一个 middleware,你可以在这里拿到 http 请求的 req、res等信息,
* 然后通过 res.write() | res.end() 返回响应数据, 否则需要执行 next() 方法。
* 在 `req` 中,还可以拿到 query、params、body, refererQuery 等已解析的请求信息。
*
* @see [connect](https://github.com/senchalabs/connect#appusefn)
* @example
* ```ts
* export default {
* response(req, res) {
* res.setHeader('Content-Type', 'application/json')
* res.end(JSON.stringify({ a: 1 }))
* },
* }
* ```
*
*/
response?: (req: MockRequest, res: MockResponse, next: Connect.NextFunction) => void | Promise<void>;
/**
* Request Validator
*
* Sometimes, for the same API request, data needs to be returned based
* on different request parameters.
* However, if all of this is written in a single mock's body or response,
* the content can become cumbersome and difficult to manage.
* The function of a validator allows you to configure multiple mocks with
* the same URL simultaneously and determine which mock should be used through validation.
*
* 请求验证器
*
* 有时候,一个相同的API请求,需要根据不同的请求参数,来决定返回数据,
* 但全部都在单个 mock中的 body或者 response 中写,内容会很庞杂,不好管理,
* 验证器的功能,允许你同时配置多条相同url的mock,通过验证器来判断使哪个mock生效。
* @example
* ```ts
* export default {
* validator: {
* query: { id: 123 }
* }
* }
* ```
* @example
* ```ts
* export default {
* validator: function(request) {
* return request.query.id === 123
* }
* }
* ```
*/
validator?: Partial<Omit<ExtraRequest, "getCookie">> | ((request: ExtraRequest) => boolean);
ws?: false;
}
interface MockWebsocketItem extends MockBaseItem {
ws: true;
/**
* Configure Websocket Server
*
* 配置 Websocket Server
* @example
* ```ts
* export default {
* ws: true
* setup: (wss, { onCleanup }) => {
* wss.on('connection', (ws,req) => {
* ws.on('message', (raw) => console.log(raw))
* const timer = setInterval(
* () => ws.send(JSON.stringify({ type: 'connected' })),
* 1000,
* )
* onCleanup(() => clearInterval(timer))
* })
* wss.on('error', (error) => console.error(error))
* }
* }
* ```
*/
setup: (wss: WebSocketServer, context: WebSocketSetupContext) => void;
}
interface WebSocketSetupContext {
/**
* When defining WSS, you may perform some automatic or looping tasks.
* However, when hot updating, the plugin will re-execute `setup()`,
* which may result in duplicate registration of listening events and looping tasks
* such as setTimeout. You can use `onCleanup()` to clear these automatic or looping tasks.
*
* 当你在定义 WSS 时,可能会执行一些自动任务或循环任务,
* 但是当热更新时,插件内部会重新执行 setup() ,
* 这可能导致出现 重复注册监听事件 和 循环任务如 `setTimeout` 等。
* 通过 `onCleanup()` 可以来清除这些自动任务或循环任务。
* @example
* ``` ts
* onCleanup(() => clearTimeout(timeId))
* ```
*/
onCleanup: (cleanup: () => void) => void;
}
type MockOptions = (MockHttpItem | MockWebsocketItem)[];
type FormidableFile = formidable.File | formidable.File[];
type LogType = "info" | "warn" | "error" | "debug";
type LogLevel = LogType | "silent";
//#endregion
export { FormidableFile, LogLevel, MockHttpItem, MockOptions, MockRequest, MockServerPluginOptions, MockWebsocketItem, ServerBuildOption };