UNPKG

rspack-plugin-mock

Version:
268 lines (267 loc) 7.55 kB
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 };