rspack-plugin-mock
Version:
inject api mock server to development server
268 lines (267 loc) • 7.55 kB
JavaScript
import { deepClone, isArray, isFunction } from "@pengzhanbo/utils";
import { createHash } from "node:crypto";
import { Transform } from "node:stream";
//#region src/helpers/createSSEStream.ts
/**
* Transforms "messages" to W3C event stream content.
* See https://html.spec.whatwg.org/multipage/server-sent-events.html
* A message is an object with one or more of the following properties:
* - data (String or object, which gets turned into JSON)
* - event
* - id
* - retry
* - comment
*
* If constructed with a HTTP Request, it will optimise the socket for streaming.
* If this stream is piped to an HTTP Response, it will set appropriate headers.
*/
var SSEStream = class extends Transform {
constructor(req) {
super({ objectMode: true });
req.socket.setKeepAlive(true);
req.socket.setNoDelay(true);
req.socket.setTimeout(0);
}
pipe(destination, options) {
if (destination.writeHead) {
destination.writeHead(200, {
"Content-Type": "text/event-stream; charset=utf-8",
"Transfer-Encoding": "identity",
"Cache-Control": "no-cache",
"Connection": "keep-alive"
});
destination.flushHeaders?.();
}
destination.write(":ok\n\n");
return super.pipe(destination, options);
}
_transform(message, encoding, callback) {
if (message.comment) this.push(`: ${message.comment}\n`);
if (message.event) this.push(`event: ${message.event}\n`);
if (message.id) this.push(`id: ${message.id}\n`);
if (message.retry) this.push(`retry: ${message.retry}\n`);
if (message.data) this.push(dataString(message.data));
this.push("\n");
callback();
}
write(message, ...args) {
return super.write(message, ...args);
}
destroy(error) {
if (error) this.write({
event: "error",
data: error.message
});
this.end();
return this;
}
};
function dataString(data) {
if (typeof data === "object") return dataString(JSON.stringify(data));
return data.split(/\r\n|\r|\n/).map((line) => `data: ${line}\n`).join("");
}
/**
* 创建一个 Server-sent events 写入流,用于支持模拟 EventSource
*
* @example
* ```ts
* import { createSSEStream, defineMock } from 'vite-plugin-mock-dev-server'
*
* export default defineMock({
* url: '/api',
* response: (req, res) => {
* const sse = createSSEStream(req, res)
* sse.write({ event: 'message', data: { message: 'hello world' } })
* }
* })
* ```
*/
function createSSEStream(req, res) {
const sse = new SSEStream(req);
sse.pipe(res);
return sse;
}
//#endregion
//#region src/helpers/defineMock.ts
function defineMock(config) {
return config;
}
/**
* Return a custom defineMock function to support preprocessing of mock config.
*
* 返回一个自定义的 defineMock 函数,用于支持对 mock config 的预处理。
* @param transformer preprocessing function
* @example
* ```ts
* const definePostMock = createDefineMock((mock) => {
* mock.url = '/api/post/' + mock.url
* })
* export default definePostMock({
* url: 'list',
* body: [{ title: '1' }, { title: '2' }],
* })
* ```
*/
function createDefineMock(transformer) {
const define = (config) => {
if (isArray(config)) config = config.map((item) => transformer(item) || item);
else config = transformer(config) || config;
return config;
};
return define;
}
//#endregion
//#region src/helpers/defineMockData.ts
/**
* Since the plugin compiles `*.mock.*` files as separate independent entries,
* this leads to inconsistent dependency relationships after mock file compilation,
* with each mock file having its own scope.
* Even if multiple `*.mock.*` files import the same `data.ts` file, they are
* completely different instances of `data`, making operations on `data` not
* shareable across different `*.mock.*` files.
*
* To address this, the plugin provides a memory-based data sharing mechanism.
*
* 由于插件是分别独立对 `*.mock.*` 等文件作为单独入口进行编译的,
* 这导致了 mock 文件编译后的依赖关系不一致,每个 mock 文件拥有独立的作用域,
* 使得即使多个 `*.mock.*` 虽然引入了同一个 `data.ts` 文件,然而确是完全不同两份 `data`,
* 使得对 `data` 的操作,在不同的 `*.mock.*` 文件中并不能共享。
*
* 为此,插件提供了一种基于 memory 的数据共享机制。
*/
/**
* Mock data cache
*
* Mock 数据缓存
*/
const mockDataCache = /* @__PURE__ */ new Map();
/**
* Response cache for MockData objects
*
* MockData 对象的响应缓存
*/
const responseCache = /* @__PURE__ */ new WeakMap();
/**
* Stale interval in milliseconds
*
* 缓存过期间隔(毫秒)
*/
const staleInterval = 70;
/**
* Cache implementation for mock data
*
* Mock 数据缓存实现
*
* @template T - Type of cached data / 缓存数据的类型
*/
var CacheImpl = class {
/**
* Current cached value
*
* 当前缓存值
*/
value;
/**
* Initial value hash, used to detect if initial data has changed
*
* 初始化数据的哈希值,用于判断传入的初始化数据是否发生变更
*/
#hash;
/**
* Last update timestamp
*
* 最后更新时间戳
*/
#lastUpdate;
/**
* Whether to persist data on HMR
*
* 热更新时是否保持数据
*/
#persistOnHMR;
/**
* Constructor
*
* 构造函数
*
* @param value - Initial value / 初始值
* @param persistOnHMR - Whether to persist data on HMR / 热更新时是否保持数据
*/
constructor(value, persistOnHMR) {
this.value = value;
this.#hash = getHash(value);
this.#lastUpdate = Date.now();
this.#persistOnHMR = persistOnHMR ?? false;
}
/**
* Hot update cached value
*
* 热更新缓存值
*
* @param value - New value / 新值
*/
hotUpdate(value) {
if (this.#persistOnHMR) return;
if (Date.now() - this.#lastUpdate < staleInterval) return;
const hash = getHash(value);
if (this.#hash !== hash) {
this.value = deepClone(value);
this.#hash = hash;
this.#lastUpdate = Date.now();
}
}
/**
* Set persistOnHMR
*
* 设置 persistOnHMR
*
* @param persistOnHMR - Whether to persist data on HMR / 热更新时是否保持数据
*/
setPersistOnHMR(persistOnHMR) {
if (!this.#persistOnHMR) this.#persistOnHMR = persistOnHMR;
}
};
/**
* Define mock data with memory-based sharing mechanism
*
* 定义带有基于内存的共享机制的 Mock 数据
*
* @template T - Type of mock data / Mock 数据的类型
* @param key - Unique key for mock data / Mock 数据的唯一键
* @param initialData - Initial data value / 初始数据值
* @param options - Options / 选项
* @returns MockData object with getter, setter, and value property / 带有 getter、setter 和 value 属性的 MockData 对象
*/
function defineMockData(key, initialData, options) {
let cache = mockDataCache.get(key);
if (!cache) {
const newCache = new CacheImpl(initialData, options?.persistOnHMR);
const existing = mockDataCache.get(key);
if (existing) cache = existing;
else {
mockDataCache.set(key, newCache);
cache = newCache;
}
} else cache.setPersistOnHMR(options?.persistOnHMR ?? false);
cache.hotUpdate(initialData);
if (responseCache.has(cache)) return responseCache.get(cache);
const res = [() => cache.value, (val) => {
if (isFunction(val)) val = val(cache.value) ?? cache.value;
cache.value = val;
}];
Object.defineProperty(res, "value", {
get() {
return cache.value;
},
set(val) {
cache.value = val;
}
});
responseCache.set(cache, res);
return res;
}
function getHash(data) {
return createHash("sha256").update(JSON.stringify(data)).digest("hex");
}
//#endregion
export { createDefineMock, createSSEStream, defineMock, defineMockData };