vite-plugin-ai-mock 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,350 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ aiMockPlugin: () => aiMockPlugin
34
+ });
35
+ module.exports = __toCommonJS(index_exports);
36
+ var import_node_fs = __toESM(require("fs"), 1);
37
+ var import_node_path = __toESM(require("path"), 1);
38
+ var AI_MOCK_BASE = "/api/ai/mock";
39
+ var SCENARIO_PRESETS = {
40
+ normal: {},
41
+ "first-delay": { firstChunkDelayMs: 1800 },
42
+ jitter: { minIntervalMs: 80, maxIntervalMs: 1400 },
43
+ disconnect: { disconnectAt: 3 },
44
+ timeout: { stallAfter: 2, stallMs: 3e4 },
45
+ error: { errorAt: 2, errorMessage: "mock_error" },
46
+ malformed: { malformedAt: 2 },
47
+ duplicate: { duplicateAt: 2 },
48
+ "out-of-order": { outOfOrder: true },
49
+ reconnect: { reconnect: true },
50
+ heartbeat: { heartbeatMs: 2500 }
51
+ };
52
+ function clampPositiveInt(value, fallback) {
53
+ if (!value) return fallback;
54
+ const n = Number.parseInt(value, 10);
55
+ return Number.isFinite(n) && n >= 0 ? n : fallback;
56
+ }
57
+ function readJsonFile(filePath) {
58
+ const content = import_node_fs.default.readFileSync(filePath, "utf-8");
59
+ return JSON.parse(content);
60
+ }
61
+ function safeFileName(name) {
62
+ return name.replace(/[^a-zA-Z0-9._-]/g, "");
63
+ }
64
+ function resolveDataFile(dataDir, fileName) {
65
+ const safeName = safeFileName(fileName) || "default";
66
+ const absoluteDataDir = import_node_path.default.resolve(process.cwd(), dataDir);
67
+ const candidate = safeName.endsWith(".json") ? import_node_path.default.join(absoluteDataDir, safeName) : import_node_path.default.join(absoluteDataDir, `${safeName}.json`);
68
+ if (!candidate.startsWith(absoluteDataDir)) {
69
+ throw new Error("Invalid mock file path.");
70
+ }
71
+ if (!import_node_fs.default.existsSync(candidate)) {
72
+ throw new Error(`Mock data file not found: ${import_node_path.default.basename(candidate)}`);
73
+ }
74
+ return candidate;
75
+ }
76
+ function normalizeChunks(raw) {
77
+ const source = Array.isArray(raw) ? raw : typeof raw === "object" && raw !== null && "chunks" in raw ? raw.chunks : [raw];
78
+ if (!Array.isArray(source)) return [];
79
+ return source.map((item, index) => {
80
+ if (typeof item === "object" && item !== null) {
81
+ const chunk = item;
82
+ return {
83
+ id: String(chunk.id ?? index + 1),
84
+ event: chunk.event ?? "message",
85
+ data: chunk.data ?? null,
86
+ delayMs: chunk.delayMs
87
+ };
88
+ }
89
+ return {
90
+ id: String(index + 1),
91
+ event: "message",
92
+ data: item
93
+ };
94
+ });
95
+ }
96
+ function parseScenarioOptions(reqUrl, lastEventIdHeader, defaultScenario) {
97
+ const params = reqUrl.searchParams;
98
+ const presetName = params.get("scenario") ?? defaultScenario?.scenario;
99
+ const preset = presetName ? SCENARIO_PRESETS[presetName] ?? {} : {};
100
+ const getParam = (paramName, fallback) => {
101
+ const paramValue = params.get(String(paramName));
102
+ if (paramValue !== null) {
103
+ return typeof fallback === "number" ? clampPositiveInt(paramValue, fallback) : paramValue;
104
+ }
105
+ if (defaultScenario && paramName in defaultScenario) {
106
+ return defaultScenario[paramName] ?? fallback;
107
+ }
108
+ const presetValue = preset[String(paramName)];
109
+ if (presetValue !== void 0) {
110
+ return presetValue;
111
+ }
112
+ return fallback;
113
+ };
114
+ const firstChunkDelayMs = getParam("firstChunkDelayMs", 0);
115
+ let minIntervalMs = getParam("minIntervalMs", 200);
116
+ let maxIntervalMs = getParam("maxIntervalMs", 700);
117
+ return {
118
+ file: params.get("file") ?? "default",
119
+ firstChunkDelayMs,
120
+ minIntervalMs: Math.min(minIntervalMs, maxIntervalMs),
121
+ maxIntervalMs: Math.max(minIntervalMs, maxIntervalMs),
122
+ disconnectAt: getParam("disconnectAt", -1),
123
+ stallAfter: getParam("stallAfter", -1),
124
+ stallMs: getParam("stallMs", 3e4),
125
+ httpErrorStatus: clampPositiveInt(params.get("httpErrorStatus"), 0),
126
+ errorAt: getParam("errorAt", -1),
127
+ errorMessage: getParam("errorMessage", "mock_error"),
128
+ malformedAt: getParam("malformedAt", -1),
129
+ duplicateAt: getParam("duplicateAt", -1),
130
+ outOfOrder: params.get("outOfOrder") === "true" || Boolean(defaultScenario?.outOfOrder) || Boolean(preset.outOfOrder),
131
+ heartbeatMs: getParam("heartbeatMs", 0),
132
+ includeDone: params.get("includeDone") !== "false",
133
+ reconnect: params.get("reconnect") === "true" || Boolean(defaultScenario?.reconnect) || Boolean(preset.reconnect),
134
+ lastEventId: params.get("lastEventId") ?? lastEventIdHeader ?? null
135
+ };
136
+ }
137
+ function getResumeIndex(chunks, lastEventId) {
138
+ if (!lastEventId) return 0;
139
+ const hitIndex = chunks.findIndex((chunk) => chunk.id === lastEventId);
140
+ return hitIndex >= 0 ? hitIndex + 1 : 0;
141
+ }
142
+ function applyChunkMutations(chunks, options) {
143
+ let result = chunks.map((item) => ({ ...item }));
144
+ if (options.reconnect && options.lastEventId) {
145
+ const startIndex = getResumeIndex(result, options.lastEventId);
146
+ result = result.slice(startIndex);
147
+ }
148
+ if (options.outOfOrder && result.length > 2) {
149
+ const swapped = [...result];
150
+ const temp = swapped[1];
151
+ swapped[1] = swapped[2];
152
+ swapped[2] = temp;
153
+ result = swapped;
154
+ }
155
+ if (options.duplicateAt > 0 && options.duplicateAt <= result.length) {
156
+ const index = options.duplicateAt - 1;
157
+ result.splice(index + 1, 0, { ...result[index], id: `${result[index].id}-dup` });
158
+ }
159
+ return result;
160
+ }
161
+ function isSseRequest(req, reqUrl) {
162
+ const accept = String(req.headers.accept ?? "");
163
+ const transport = reqUrl.searchParams.get("transport");
164
+ return accept.includes("text/event-stream") || transport === "sse";
165
+ }
166
+ function writeSseEvent(res, options) {
167
+ if (options.id) res.write(`id: ${options.id}
168
+ `);
169
+ if (options.event && options.event !== "message") res.write(`event: ${options.event}
170
+ `);
171
+ const payload = typeof options.data === "string" ? options.data : JSON.stringify(options.data ?? null);
172
+ const lines = payload.split("\n");
173
+ for (const line of lines) {
174
+ res.write(`data: ${line}
175
+ `);
176
+ }
177
+ res.write("\n");
178
+ }
179
+ function matchEndpoint(pathname, endpoint) {
180
+ if (Array.isArray(endpoint)) {
181
+ for (const item of endpoint) {
182
+ const result = matchEndpoint(pathname, item);
183
+ if (result !== null) return result;
184
+ }
185
+ return null;
186
+ }
187
+ if (typeof endpoint === "string") {
188
+ if (pathname === endpoint) return { fileFromPath: "" };
189
+ if (pathname.startsWith(`${endpoint}/`)) return { fileFromPath: pathname.slice(endpoint.length + 1) };
190
+ return null;
191
+ }
192
+ return endpoint.test(pathname) ? { fileFromPath: "" } : null;
193
+ }
194
+ function aiMockPlugin(config) {
195
+ const dataDir = config?.dataDir ?? "mock/ai";
196
+ const endpoint = config?.endpoint ?? AI_MOCK_BASE;
197
+ const defaultScenario = config?.defaultScenario;
198
+ return {
199
+ name: "vite-plugin-ai-mock",
200
+ configureServer(server) {
201
+ server.middlewares.use((req, res, next) => {
202
+ if (!req.url) return next();
203
+ const reqUrl = new URL(req.url, "http://localhost");
204
+ const matched = matchEndpoint(reqUrl.pathname, endpoint);
205
+ if (matched === null) return next();
206
+ const fileFromPath = matched.fileFromPath;
207
+ const lastEventIdHeader = typeof req.headers["last-event-id"] === "string" ? req.headers["last-event-id"] : void 0;
208
+ const options = parseScenarioOptions(reqUrl, lastEventIdHeader, defaultScenario);
209
+ if (fileFromPath) options.file = fileFromPath;
210
+ try {
211
+ if (options.httpErrorStatus >= 400) {
212
+ res.statusCode = options.httpErrorStatus;
213
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
214
+ res.end(JSON.stringify({ error: "http_error", status: options.httpErrorStatus }));
215
+ return;
216
+ }
217
+ const filePath = resolveDataFile(dataDir, options.file);
218
+ const raw = readJsonFile(filePath);
219
+ const chunks = applyChunkMutations(normalizeChunks(raw), options);
220
+ if (!isSseRequest(req, reqUrl)) {
221
+ res.statusCode = 200;
222
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
223
+ res.end(
224
+ JSON.stringify({
225
+ mode: "json",
226
+ file: import_node_path.default.basename(filePath),
227
+ total: chunks.length,
228
+ options,
229
+ chunks
230
+ })
231
+ );
232
+ return;
233
+ }
234
+ res.statusCode = 200;
235
+ res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
236
+ res.setHeader("Cache-Control", "no-cache, no-transform");
237
+ res.setHeader("Connection", "keep-alive");
238
+ res.setHeader("X-Accel-Buffering", "no");
239
+ if ("flushHeaders" in res && typeof res.flushHeaders === "function") {
240
+ res.flushHeaders();
241
+ }
242
+ let closed = false;
243
+ let heartbeatTimer = null;
244
+ const pendingTimers = /* @__PURE__ */ new Set();
245
+ const cleanup = () => {
246
+ if (closed) return;
247
+ closed = true;
248
+ if (heartbeatTimer) clearInterval(heartbeatTimer);
249
+ for (const timer of pendingTimers) clearTimeout(timer);
250
+ pendingTimers.clear();
251
+ };
252
+ req.on("close", cleanup);
253
+ if (options.heartbeatMs > 0) {
254
+ heartbeatTimer = setInterval(() => {
255
+ if (closed) return;
256
+ res.write(`: ping ${Date.now()}
257
+
258
+ `);
259
+ }, options.heartbeatMs);
260
+ }
261
+ const schedule = (task, delay) => {
262
+ const timer = setTimeout(() => {
263
+ pendingTimers.delete(timer);
264
+ task();
265
+ }, delay);
266
+ pendingTimers.add(timer);
267
+ };
268
+ const writeChunk = (chunk, index) => {
269
+ if (closed) return;
270
+ const chunkNo = index + 1;
271
+ if (options.disconnectAt === chunkNo) {
272
+ cleanup();
273
+ if ("destroy" in res && typeof res.destroy === "function") {
274
+ res.destroy();
275
+ return;
276
+ }
277
+ res.end();
278
+ return;
279
+ }
280
+ if (options.errorAt === chunkNo) {
281
+ writeSseEvent(res, {
282
+ id: chunk.id,
283
+ event: "error",
284
+ data: { message: options.errorMessage, at: chunkNo }
285
+ });
286
+ cleanup();
287
+ res.end();
288
+ return;
289
+ }
290
+ if (options.malformedAt === chunkNo) {
291
+ res.write(`id: ${chunk.id}
292
+ `);
293
+ res.write("event: message\n");
294
+ res.write('data: {"malformed": true\n\n');
295
+ } else {
296
+ writeSseEvent(res, {
297
+ id: chunk.id,
298
+ event: chunk.event,
299
+ data: chunk.data
300
+ });
301
+ }
302
+ if (options.stallAfter === chunkNo) {
303
+ schedule(() => {
304
+ if (!closed) {
305
+ cleanup();
306
+ res.end();
307
+ }
308
+ }, options.stallMs);
309
+ return;
310
+ }
311
+ const nextChunk = chunks[index + 1];
312
+ if (!nextChunk) {
313
+ if (options.includeDone) {
314
+ writeSseEvent(res, { event: "done", data: { done: true } });
315
+ }
316
+ cleanup();
317
+ res.end();
318
+ return;
319
+ }
320
+ const interval = typeof nextChunk.delayMs === "number" ? nextChunk.delayMs : options.minIntervalMs + Math.floor(Math.random() * (options.maxIntervalMs - options.minIntervalMs + 1));
321
+ schedule(() => writeChunk(nextChunk, index + 1), interval);
322
+ };
323
+ if (chunks.length === 0) {
324
+ if (options.includeDone) {
325
+ writeSseEvent(res, { event: "done", data: { done: true } });
326
+ }
327
+ cleanup();
328
+ res.end();
329
+ return;
330
+ }
331
+ schedule(() => writeChunk(chunks[0], 0), options.firstChunkDelayMs);
332
+ } catch (error) {
333
+ res.statusCode = 500;
334
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
335
+ res.end(
336
+ JSON.stringify({
337
+ error: "mock_server_error",
338
+ message: error instanceof Error ? error.message : "Unknown error"
339
+ })
340
+ );
341
+ }
342
+ });
343
+ }
344
+ };
345
+ }
346
+ // Annotate the CommonJS export names for ESM import in node:
347
+ 0 && (module.exports = {
348
+ aiMockPlugin
349
+ });
350
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport type { Plugin } from \"vite\";\n\ntype ChunkValue = string | number | boolean | Record<string, unknown> | null;\n\ninterface SourceChunk {\n id?: string | number;\n event?: string;\n data?: ChunkValue;\n delayMs?: number;\n}\n\ninterface NormalizedChunk {\n id: string;\n event: string;\n data: ChunkValue;\n delayMs?: number;\n}\n\ninterface ScenarioOptions {\n file: string;\n firstChunkDelayMs: number;\n minIntervalMs: number;\n maxIntervalMs: number;\n disconnectAt: number;\n stallAfter: number;\n stallMs: number;\n httpErrorStatus: number;\n errorAt: number;\n errorMessage: string;\n malformedAt: number;\n duplicateAt: number;\n outOfOrder: boolean;\n heartbeatMs: number;\n includeDone: boolean;\n reconnect: boolean;\n lastEventId: string | null;\n}\n\nexport type EndpointPattern = string | RegExp | (string | RegExp)[];\n\nexport interface AiMockPluginOptions {\n dataDir?: string;\n endpoint?: EndpointPattern;\n /**\n * Default scenario configuration for all mock requests.\n * If set, all requests will use this scenario unless overridden by URL parameters.\n * @default undefined (uses 'normal' scenario with no preset)\n */\n defaultScenario?: DefaultScenarioConfig;\n}\n\nconst AI_MOCK_BASE = \"/api/ai/mock\";\n\nconst SCENARIO_PRESETS = {\n normal: {},\n \"first-delay\": { firstChunkDelayMs: 1800 },\n jitter: { minIntervalMs: 80, maxIntervalMs: 1400 },\n disconnect: { disconnectAt: 3 },\n timeout: { stallAfter: 2, stallMs: 30_000 },\n error: { errorAt: 2, errorMessage: \"mock_error\" },\n malformed: { malformedAt: 2 },\n duplicate: { duplicateAt: 2 },\n \"out-of-order\": { outOfOrder: true },\n reconnect: { reconnect: true },\n heartbeat: { heartbeatMs: 2500 },\n} as const;\n\nexport type ScenarioName = keyof typeof SCENARIO_PRESETS;\n\nexport interface DefaultScenarioConfig extends Partial<Omit<ScenarioOptions, 'file' | 'lastEventId' | 'includeDone'>> {\n scenario?: ScenarioName;\n}\n\nfunction clampPositiveInt(value: string | null, fallback: number): number {\n if (!value) return fallback;\n const n = Number.parseInt(value, 10);\n return Number.isFinite(n) && n >= 0 ? n : fallback;\n}\n\nfunction readJsonFile(filePath: string): unknown {\n const content = fs.readFileSync(filePath, \"utf-8\");\n return JSON.parse(content);\n}\n\nfunction safeFileName(name: string): string {\n return name.replace(/[^a-zA-Z0-9._-]/g, \"\");\n}\n\nfunction resolveDataFile(dataDir: string, fileName: string): string {\n const safeName = safeFileName(fileName) || \"default\";\n const absoluteDataDir = path.resolve(process.cwd(), dataDir);\n const candidate = safeName.endsWith(\".json\")\n ? path.join(absoluteDataDir, safeName)\n : path.join(absoluteDataDir, `${safeName}.json`);\n\n if (!candidate.startsWith(absoluteDataDir)) {\n throw new Error(\"Invalid mock file path.\");\n }\n\n if (!fs.existsSync(candidate)) {\n throw new Error(`Mock data file not found: ${path.basename(candidate)}`);\n }\n\n return candidate;\n}\n\nfunction normalizeChunks(raw: unknown): NormalizedChunk[] {\n const source = Array.isArray(raw)\n ? raw\n : typeof raw === \"object\" && raw !== null && \"chunks\" in raw\n ? (raw as { chunks: unknown }).chunks\n : [raw];\n\n if (!Array.isArray(source)) return [];\n\n return source.map((item, index) => {\n if (typeof item === \"object\" && item !== null) {\n const chunk = item as SourceChunk;\n return {\n id: String(chunk.id ?? index + 1),\n event: chunk.event ?? \"message\",\n data: chunk.data ?? null,\n delayMs: chunk.delayMs,\n };\n }\n\n return {\n id: String(index + 1),\n event: \"message\",\n data: item as ChunkValue,\n };\n });\n}\n\nfunction parseScenarioOptions(\n reqUrl: URL,\n lastEventIdHeader: string | undefined,\n defaultScenario?: DefaultScenarioConfig,\n): ScenarioOptions {\n const params = reqUrl.searchParams;\n\n // Determine effective scenario: URL param > defaultScenario.scenario > none\n const presetName = (params.get(\"scenario\") as ScenarioName | null)\n ?? defaultScenario?.scenario;\n const preset = presetName ? SCENARIO_PRESETS[presetName] ?? {} : {};\n\n // Helper to get value from URL param > defaultScenario > preset\n const getParam = (\n paramName: keyof ScenarioOptions,\n fallback: number | string | boolean,\n ): number | string | boolean => {\n const paramValue = params.get(String(paramName));\n if (paramValue !== null) {\n return typeof fallback === \"number\" ? clampPositiveInt(paramValue, fallback) : paramValue;\n }\n if (defaultScenario && paramName in defaultScenario) {\n return defaultScenario[paramName as keyof DefaultScenarioConfig] ?? fallback;\n }\n const presetValue = (preset as Record<string, unknown>)[String(paramName)];\n if (presetValue !== undefined) {\n return presetValue as number | string | boolean;\n }\n return fallback;\n };\n\n const firstChunkDelayMs = getParam(\"firstChunkDelayMs\", 0) as number;\n let minIntervalMs = getParam(\"minIntervalMs\", 200) as number;\n let maxIntervalMs = getParam(\"maxIntervalMs\", 700) as number;\n\n return {\n file: params.get(\"file\") ?? \"default\",\n firstChunkDelayMs,\n minIntervalMs: Math.min(minIntervalMs, maxIntervalMs),\n maxIntervalMs: Math.max(minIntervalMs, maxIntervalMs),\n disconnectAt: getParam(\"disconnectAt\", -1) as number,\n stallAfter: getParam(\"stallAfter\", -1) as number,\n stallMs: getParam(\"stallMs\", 30_000) as number,\n httpErrorStatus: clampPositiveInt(params.get(\"httpErrorStatus\"), 0),\n errorAt: getParam(\"errorAt\", -1) as number,\n errorMessage: (getParam(\"errorMessage\", \"mock_error\") as string),\n malformedAt: getParam(\"malformedAt\", -1) as number,\n duplicateAt: getParam(\"duplicateAt\", -1) as number,\n outOfOrder:\n params.get(\"outOfOrder\") === \"true\" ||\n Boolean(defaultScenario?.outOfOrder) ||\n Boolean((preset as { outOfOrder?: boolean }).outOfOrder),\n heartbeatMs: getParam(\"heartbeatMs\", 0) as number,\n includeDone: params.get(\"includeDone\") !== \"false\",\n reconnect:\n params.get(\"reconnect\") === \"true\" ||\n Boolean(defaultScenario?.reconnect) ||\n Boolean((preset as { reconnect?: boolean }).reconnect),\n lastEventId: params.get(\"lastEventId\") ?? lastEventIdHeader ?? null,\n };\n}\n\nfunction getResumeIndex(chunks: NormalizedChunk[], lastEventId: string | null): number {\n if (!lastEventId) return 0;\n const hitIndex = chunks.findIndex((chunk) => chunk.id === lastEventId);\n return hitIndex >= 0 ? hitIndex + 1 : 0;\n}\n\nfunction applyChunkMutations(chunks: NormalizedChunk[], options: ScenarioOptions): NormalizedChunk[] {\n let result = chunks.map((item) => ({ ...item }));\n\n if (options.reconnect && options.lastEventId) {\n const startIndex = getResumeIndex(result, options.lastEventId);\n result = result.slice(startIndex);\n }\n\n if (options.outOfOrder && result.length > 2) {\n const swapped = [...result];\n const temp = swapped[1];\n swapped[1] = swapped[2];\n swapped[2] = temp;\n result = swapped;\n }\n\n if (options.duplicateAt > 0 && options.duplicateAt <= result.length) {\n const index = options.duplicateAt - 1;\n result.splice(index + 1, 0, { ...result[index], id: `${result[index].id}-dup` });\n }\n\n return result;\n}\n\nfunction isSseRequest(req: { headers: Record<string, string | string[] | undefined> }, reqUrl: URL): boolean {\n const accept = String(req.headers.accept ?? \"\");\n const transport = reqUrl.searchParams.get(\"transport\");\n return accept.includes(\"text/event-stream\") || transport === \"sse\";\n}\n\nfunction writeSseEvent(\n res: {\n write: (chunk: string) => void;\n },\n options: { id?: string; event?: string; data: unknown },\n): void {\n if (options.id) res.write(`id: ${options.id}\\n`);\n if (options.event && options.event !== \"message\") res.write(`event: ${options.event}\\n`);\n\n const payload = typeof options.data === \"string\" ? options.data : JSON.stringify(options.data ?? null);\n const lines = payload.split(\"\\n\");\n for (const line of lines) {\n res.write(`data: ${line}\\n`);\n }\n res.write(\"\\n\");\n}\n\ninterface EndpointMatchResult {\n fileFromPath: string;\n}\n\nfunction matchEndpoint(pathname: string, endpoint: EndpointPattern): EndpointMatchResult | null {\n if (Array.isArray(endpoint)) {\n for (const item of endpoint) {\n const result = matchEndpoint(pathname, item);\n if (result !== null) return result;\n }\n return null;\n }\n if (typeof endpoint === \"string\") {\n if (pathname === endpoint) return { fileFromPath: \"\" };\n if (pathname.startsWith(`${endpoint}/`)) return { fileFromPath: pathname.slice(endpoint.length + 1) };\n return null;\n }\n // RegExp: fileFromPath falls back to empty string, relies on ?file= param\n return endpoint.test(pathname) ? { fileFromPath: \"\" } : null;\n}\n\nexport function aiMockPlugin(config?: AiMockPluginOptions): Plugin {\n const dataDir = config?.dataDir ?? \"mock/ai\";\n const endpoint: EndpointPattern = config?.endpoint ?? AI_MOCK_BASE;\n const defaultScenario = config?.defaultScenario;\n\n return {\n name: \"vite-plugin-ai-mock\",\n configureServer(server) {\n server.middlewares.use((req, res, next) => {\n if (!req.url) return next();\n const reqUrl = new URL(req.url, \"http://localhost\");\n const matched = matchEndpoint(reqUrl.pathname, endpoint);\n if (matched === null) return next();\n const fileFromPath = matched.fileFromPath;\n\n const lastEventIdHeader =\n typeof req.headers[\"last-event-id\"] === \"string\" ? req.headers[\"last-event-id\"] : undefined;\n\n const options = parseScenarioOptions(reqUrl, lastEventIdHeader, defaultScenario);\n if (fileFromPath) options.file = fileFromPath;\n\n try {\n if (options.httpErrorStatus >= 400) {\n res.statusCode = options.httpErrorStatus;\n res.setHeader(\"Content-Type\", \"application/json; charset=utf-8\");\n res.end(JSON.stringify({ error: \"http_error\", status: options.httpErrorStatus }));\n return;\n }\n\n const filePath = resolveDataFile(dataDir, options.file);\n const raw = readJsonFile(filePath);\n const chunks = applyChunkMutations(normalizeChunks(raw), options);\n\n if (!isSseRequest(req, reqUrl)) {\n res.statusCode = 200;\n res.setHeader(\"Content-Type\", \"application/json; charset=utf-8\");\n res.end(\n JSON.stringify({\n mode: \"json\",\n file: path.basename(filePath),\n total: chunks.length,\n options,\n chunks,\n }),\n );\n return;\n }\n\n res.statusCode = 200;\n res.setHeader(\"Content-Type\", \"text/event-stream; charset=utf-8\");\n res.setHeader(\"Cache-Control\", \"no-cache, no-transform\");\n res.setHeader(\"Connection\", \"keep-alive\");\n res.setHeader(\"X-Accel-Buffering\", \"no\");\n if (\"flushHeaders\" in res && typeof res.flushHeaders === \"function\") {\n res.flushHeaders();\n }\n\n let closed = false;\n let heartbeatTimer: NodeJS.Timeout | null = null;\n const pendingTimers = new Set<NodeJS.Timeout>();\n\n const cleanup = () => {\n if (closed) return;\n closed = true;\n if (heartbeatTimer) clearInterval(heartbeatTimer);\n for (const timer of pendingTimers) clearTimeout(timer);\n pendingTimers.clear();\n };\n\n req.on(\"close\", cleanup);\n\n if (options.heartbeatMs > 0) {\n heartbeatTimer = setInterval(() => {\n if (closed) return;\n res.write(`: ping ${Date.now()}\\n\\n`);\n }, options.heartbeatMs);\n }\n\n const schedule = (task: () => void, delay: number) => {\n const timer = setTimeout(() => {\n pendingTimers.delete(timer);\n task();\n }, delay);\n pendingTimers.add(timer);\n };\n\n const writeChunk = (chunk: NormalizedChunk, index: number) => {\n if (closed) return;\n const chunkNo = index + 1;\n\n if (options.disconnectAt === chunkNo) {\n cleanup();\n if (\"destroy\" in res && typeof res.destroy === \"function\") {\n res.destroy();\n return;\n }\n res.end();\n return;\n }\n\n if (options.errorAt === chunkNo) {\n writeSseEvent(res, {\n id: chunk.id,\n event: \"error\",\n data: { message: options.errorMessage, at: chunkNo },\n });\n cleanup();\n res.end();\n return;\n }\n\n if (options.malformedAt === chunkNo) {\n res.write(`id: ${chunk.id}\\n`);\n res.write(\"event: message\\n\");\n res.write('data: {\"malformed\": true\\n\\n');\n } else {\n writeSseEvent(res, {\n id: chunk.id,\n event: chunk.event,\n data: chunk.data,\n });\n }\n\n if (options.stallAfter === chunkNo) {\n schedule(() => {\n if (!closed) {\n cleanup();\n res.end();\n }\n }, options.stallMs);\n return;\n }\n\n const nextChunk = chunks[index + 1];\n if (!nextChunk) {\n if (options.includeDone) {\n writeSseEvent(res, { event: \"done\", data: { done: true } });\n }\n cleanup();\n res.end();\n return;\n }\n\n const interval =\n typeof nextChunk.delayMs === \"number\"\n ? nextChunk.delayMs\n : options.minIntervalMs + Math.floor(Math.random() * (options.maxIntervalMs - options.minIntervalMs + 1));\n\n schedule(() => writeChunk(nextChunk, index + 1), interval);\n };\n\n if (chunks.length === 0) {\n if (options.includeDone) {\n writeSseEvent(res, { event: \"done\", data: { done: true } });\n }\n cleanup();\n res.end();\n return;\n }\n\n schedule(() => writeChunk(chunks[0], 0), options.firstChunkDelayMs);\n } catch (error) {\n res.statusCode = 500;\n res.setHeader(\"Content-Type\", \"application/json; charset=utf-8\");\n res.end(\n JSON.stringify({\n error: \"mock_server_error\",\n message: error instanceof Error ? error.message : \"Unknown error\",\n }),\n );\n }\n });\n },\n };\n}\n"],"mappings":"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","names":["fs","path"]}
@@ -0,0 +1,75 @@
1
+ import { Plugin } from 'vite';
2
+
3
+ interface ScenarioOptions {
4
+ file: string;
5
+ firstChunkDelayMs: number;
6
+ minIntervalMs: number;
7
+ maxIntervalMs: number;
8
+ disconnectAt: number;
9
+ stallAfter: number;
10
+ stallMs: number;
11
+ httpErrorStatus: number;
12
+ errorAt: number;
13
+ errorMessage: string;
14
+ malformedAt: number;
15
+ duplicateAt: number;
16
+ outOfOrder: boolean;
17
+ heartbeatMs: number;
18
+ includeDone: boolean;
19
+ reconnect: boolean;
20
+ lastEventId: string | null;
21
+ }
22
+ type EndpointPattern = string | RegExp | (string | RegExp)[];
23
+ interface AiMockPluginOptions {
24
+ dataDir?: string;
25
+ endpoint?: EndpointPattern;
26
+ /**
27
+ * Default scenario configuration for all mock requests.
28
+ * If set, all requests will use this scenario unless overridden by URL parameters.
29
+ * @default undefined (uses 'normal' scenario with no preset)
30
+ */
31
+ defaultScenario?: DefaultScenarioConfig;
32
+ }
33
+ declare const SCENARIO_PRESETS: {
34
+ readonly normal: {};
35
+ readonly "first-delay": {
36
+ readonly firstChunkDelayMs: 1800;
37
+ };
38
+ readonly jitter: {
39
+ readonly minIntervalMs: 80;
40
+ readonly maxIntervalMs: 1400;
41
+ };
42
+ readonly disconnect: {
43
+ readonly disconnectAt: 3;
44
+ };
45
+ readonly timeout: {
46
+ readonly stallAfter: 2;
47
+ readonly stallMs: 30000;
48
+ };
49
+ readonly error: {
50
+ readonly errorAt: 2;
51
+ readonly errorMessage: "mock_error";
52
+ };
53
+ readonly malformed: {
54
+ readonly malformedAt: 2;
55
+ };
56
+ readonly duplicate: {
57
+ readonly duplicateAt: 2;
58
+ };
59
+ readonly "out-of-order": {
60
+ readonly outOfOrder: true;
61
+ };
62
+ readonly reconnect: {
63
+ readonly reconnect: true;
64
+ };
65
+ readonly heartbeat: {
66
+ readonly heartbeatMs: 2500;
67
+ };
68
+ };
69
+ type ScenarioName = keyof typeof SCENARIO_PRESETS;
70
+ interface DefaultScenarioConfig extends Partial<Omit<ScenarioOptions, 'file' | 'lastEventId' | 'includeDone'>> {
71
+ scenario?: ScenarioName;
72
+ }
73
+ declare function aiMockPlugin(config?: AiMockPluginOptions): Plugin;
74
+
75
+ export { type AiMockPluginOptions, type DefaultScenarioConfig, type EndpointPattern, type ScenarioName, aiMockPlugin };
@@ -0,0 +1,75 @@
1
+ import { Plugin } from 'vite';
2
+
3
+ interface ScenarioOptions {
4
+ file: string;
5
+ firstChunkDelayMs: number;
6
+ minIntervalMs: number;
7
+ maxIntervalMs: number;
8
+ disconnectAt: number;
9
+ stallAfter: number;
10
+ stallMs: number;
11
+ httpErrorStatus: number;
12
+ errorAt: number;
13
+ errorMessage: string;
14
+ malformedAt: number;
15
+ duplicateAt: number;
16
+ outOfOrder: boolean;
17
+ heartbeatMs: number;
18
+ includeDone: boolean;
19
+ reconnect: boolean;
20
+ lastEventId: string | null;
21
+ }
22
+ type EndpointPattern = string | RegExp | (string | RegExp)[];
23
+ interface AiMockPluginOptions {
24
+ dataDir?: string;
25
+ endpoint?: EndpointPattern;
26
+ /**
27
+ * Default scenario configuration for all mock requests.
28
+ * If set, all requests will use this scenario unless overridden by URL parameters.
29
+ * @default undefined (uses 'normal' scenario with no preset)
30
+ */
31
+ defaultScenario?: DefaultScenarioConfig;
32
+ }
33
+ declare const SCENARIO_PRESETS: {
34
+ readonly normal: {};
35
+ readonly "first-delay": {
36
+ readonly firstChunkDelayMs: 1800;
37
+ };
38
+ readonly jitter: {
39
+ readonly minIntervalMs: 80;
40
+ readonly maxIntervalMs: 1400;
41
+ };
42
+ readonly disconnect: {
43
+ readonly disconnectAt: 3;
44
+ };
45
+ readonly timeout: {
46
+ readonly stallAfter: 2;
47
+ readonly stallMs: 30000;
48
+ };
49
+ readonly error: {
50
+ readonly errorAt: 2;
51
+ readonly errorMessage: "mock_error";
52
+ };
53
+ readonly malformed: {
54
+ readonly malformedAt: 2;
55
+ };
56
+ readonly duplicate: {
57
+ readonly duplicateAt: 2;
58
+ };
59
+ readonly "out-of-order": {
60
+ readonly outOfOrder: true;
61
+ };
62
+ readonly reconnect: {
63
+ readonly reconnect: true;
64
+ };
65
+ readonly heartbeat: {
66
+ readonly heartbeatMs: 2500;
67
+ };
68
+ };
69
+ type ScenarioName = keyof typeof SCENARIO_PRESETS;
70
+ interface DefaultScenarioConfig extends Partial<Omit<ScenarioOptions, 'file' | 'lastEventId' | 'includeDone'>> {
71
+ scenario?: ScenarioName;
72
+ }
73
+ declare function aiMockPlugin(config?: AiMockPluginOptions): Plugin;
74
+
75
+ export { type AiMockPluginOptions, type DefaultScenarioConfig, type EndpointPattern, type ScenarioName, aiMockPlugin };