vite-plugin-ai-mock 0.1.1 → 0.1.2

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/README.md CHANGED
@@ -18,11 +18,41 @@ A standalone Vite plugin for AI scene mocking. Returns streaming data in JSON fo
18
18
  ## Install
19
19
 
20
20
  ```bash
21
- npm i vite-plugin-ai-mock -D
21
+ pnpm add vite-plugin-ai-mock -D
22
22
  ```
23
23
 
24
24
  ## Usage
25
25
 
26
+ <table>
27
+ <tr>
28
+ <td width="35%" valign="top">
29
+
30
+ **Directory Structure**
31
+
32
+ ```
33
+ project/
34
+ ├── mock/
35
+ │ └── ai/
36
+ │ ├── chat.json
37
+ │ └── default.json
38
+ ├── src/
39
+ └── vite.config.ts
40
+
41
+
42
+
43
+
44
+
45
+
46
+
47
+
48
+
49
+ ```
50
+
51
+ </td>
52
+ <td width="65%" valign="top">
53
+
54
+ **vite.config.ts**
55
+
26
56
  ```ts
27
57
  import { defineConfig } from "vite";
28
58
  import { aiMockPlugin } from "vite-plugin-ai-mock";
@@ -31,12 +61,31 @@ export default defineConfig({
31
61
  plugins: [
32
62
  aiMockPlugin({
33
63
  dataDir: "mock/ai",
34
- endpoint: "/api/ai/mock",
64
+ endpoint: "/api/mock/ai", // /api/mock/ai/chat → chat.json
35
65
  }),
36
66
  ],
37
67
  });
38
68
  ```
39
69
 
70
+ **mock/ai/chat.json**
71
+
72
+ ```json
73
+ {
74
+ "chunks": [
75
+ { "id": "1", "data": { "type": "start" } },
76
+ { "id": "2", "data": { "type": "text-delta", "delta": "Hello" } },
77
+ { "id": "3", "data": { "type": "text-delta", "delta": " World!" } },
78
+ { "id": "4", "data": { "type": "finish" } }
79
+ ]
80
+ }
81
+ ```
82
+
83
+ </td>
84
+ </tr>
85
+ </table>
86
+
87
+ > 💡 See full examples: [examples](https://github.com/quanzhiyuan/vite-plugin-ai-mock/tree/main/examples) (includes Ant Design X, Assistant UI, Lobe Chat integrations)
88
+
40
89
  ## Scenarios (11)
41
90
 
42
91
  1. Normal completion (default)
@@ -65,12 +114,12 @@ Scenarios can be configured in two ways, in order of precedence:
65
114
  Append parameters directly to the request URL, useful for debugging a single endpoint:
66
115
 
67
116
  ```
68
- /api/ai/mock/default?scenario=jitter
69
- /api/ai/mock/default?firstChunkDelayMs=1000&errorAt=3
117
+ /api/mock/ai/default?scenario=jitter
118
+ /api/mock/ai/default?firstChunkDelayMs=1000&errorAt=3
70
119
  ```
71
120
 
72
121
  ```ts
73
- const response = await fetch("/api/ai/mock/default?firstChunkDelayMs=4800", {
122
+ const response = await fetch("/api/mock/ai/default?firstChunkDelayMs=4800", {
74
123
  method: "POST",
75
124
  headers: {
76
125
  "Content-Type": "application/json",
@@ -127,16 +176,16 @@ Each file is a JSON object with a `chunks` array. Every chunk maps to one SSE ev
127
176
 
128
177
  The `data` field can mirror any real API response. The package ships with ready-to-use examples in `mock/ai/` — copy them into your project as a starting point:
129
178
 
130
- | File | Provider |
131
- | --- | --- |
132
- | `mock/ai/openai.json` | OpenAI / compatible |
133
- | `mock/ai/claude.json` | Anthropic Claude |
134
- | `mock/ai/gemini.json` | Google Gemini |
135
- | `mock/ai/deepseek.json` | DeepSeek |
136
- | `mock/ai/deepseek-reasoner.json` | DeepSeek Reasoner |
137
- | `mock/ai/qwen.json` | Qwen (Alibaba) |
138
- | `mock/ai/qwen-thinking.json` | Qwen Thinking |
139
- | `mock/ai/doubao.json` | Doubao (ByteDance) |
179
+ | File | Provider |
180
+ | -------------------------------- | ------------------- |
181
+ | `mock/ai/openai.json` | OpenAI / compatible |
182
+ | `mock/ai/claude.json` | Anthropic Claude |
183
+ | `mock/ai/gemini.json` | Google Gemini |
184
+ | `mock/ai/deepseek.json` | DeepSeek |
185
+ | `mock/ai/deepseek-reasoner.json` | DeepSeek Reasoner |
186
+ | `mock/ai/qwen.json` | Qwen (Alibaba) |
187
+ | `mock/ai/qwen-thinking.json` | Qwen Thinking |
188
+ | `mock/ai/doubao.json` | Doubao (ByteDance) |
140
189
 
141
190
  **OpenAI / compatible** (`openai.json`) — `data` ends with `"[DONE]"` string:
142
191
 
@@ -185,6 +234,58 @@ The `data` field can mirror any real API response. The package ships with ready-
185
234
  }
186
235
  ```
187
236
 
237
+ **AI SDK `useChat`** — compatible with `@ai-sdk/react` `useChat` hook:
238
+
239
+ ```json
240
+ {
241
+ "chunks": [
242
+ { "id": "1", "event": "message", "data": { "type": "start" } },
243
+ {
244
+ "id": "2",
245
+ "event": "message",
246
+ "data": { "type": "text-start", "id": "t1" }
247
+ },
248
+ {
249
+ "id": "3",
250
+ "event": "message",
251
+ "data": { "type": "text-delta", "id": "t1", "delta": "Hello" }
252
+ },
253
+ {
254
+ "id": "4",
255
+ "event": "message",
256
+ "data": { "type": "text-delta", "id": "t1", "delta": ", world!" }
257
+ },
258
+ {
259
+ "id": "5",
260
+ "event": "message",
261
+ "data": { "type": "text-end", "id": "t1" }
262
+ },
263
+ {
264
+ "id": "6",
265
+ "event": "message",
266
+ "data": { "type": "finish", "finishReason": "stop" }
267
+ }
268
+ ]
269
+ }
270
+ ```
271
+
272
+ Usage with `useChat`:
273
+
274
+ ```tsx
275
+ import { useChat } from "@ai-sdk/react";
276
+ import { DefaultChatTransport } from "ai";
277
+
278
+ const { messages, sendMessage, status } = useChat({
279
+ transport: new DefaultChatTransport({
280
+ api: "/api/mock/ai/chat",
281
+ headers: {
282
+ "Content-Type": "application/json",
283
+ Accept: "text/event-stream",
284
+ },
285
+ }),
286
+ });
287
+ ```
288
+
188
289
  ## Endpoint
189
290
 
190
291
  `endpoint` accepts a `string`, `RegExp`, or `(string | RegExp)[]`.
@@ -197,10 +298,10 @@ The `data` field can mirror any real API response. The package ships with ready-
197
298
 
198
299
  ```ts
199
300
  // string (default)
200
- endpoint: "/api/ai/mock";
201
- // /api/ai/mock → file = "default"
202
- // /api/ai/mock/chat → file = "chat"
203
- // /api/ai/mock/deepseek → file = "deepseek"
301
+ endpoint: "/api/mock/ai";
302
+ // /api/mock/ai → file = "default"
303
+ // /api/mock/ai/chat → file = "chat"
304
+ // /api/mock/ai/deepseek → file = "deepseek"
204
305
 
205
306
  // RegExp
206
307
  endpoint: /^\/api\/ai\/.*/;
@@ -210,26 +311,26 @@ endpoint: /^\/api\/ai\/.*/;
210
311
  endpoint: ["/api/chat", /^\/v2\/ai\/.*/];
211
312
  ```
212
313
 
213
- - `/api/ai/mock`
214
- - `/api/ai/mock/<file>`
314
+ - `/api/mock/ai`
315
+ - `/api/mock/ai/<file>`
215
316
  - `?file=<file>`
216
317
 
217
318
  ## Test
218
319
 
219
320
  ```bash
220
- npm test
321
+ pnpm test
221
322
  ```
222
323
 
223
324
  ## Build
224
325
 
225
326
  ```bash
226
- npm run build
327
+ pnpm build
227
328
  ```
228
329
 
229
330
  ## Publish
230
331
 
231
332
  ```bash
232
- npm run release:npm
333
+ pnpm release:npm
233
334
  ```
234
335
 
235
336
  `prepublishOnly` will automatically run build, tests and typecheck..
package/README.zh-CN.md CHANGED
@@ -12,11 +12,41 @@
12
12
  ## 安装
13
13
 
14
14
  ```bash
15
- npm i vite-plugin-ai-mock -D
15
+ pnpm add vite-plugin-ai-mock -D
16
16
  ```
17
17
 
18
18
  ## 使用
19
19
 
20
+ <table>
21
+ <tr>
22
+ <td width="35%" valign="top">
23
+
24
+ **目录结构**
25
+
26
+ ```
27
+ project/
28
+ ├── mock/
29
+ │ └── ai/
30
+ │ ├── chat.json
31
+ │ └── default.json
32
+ ├── src/
33
+ └── vite.config.ts
34
+
35
+
36
+
37
+
38
+
39
+
40
+
41
+
42
+
43
+ ```
44
+
45
+ </td>
46
+ <td width="65%" valign="top">
47
+
48
+ **vite.config.ts**
49
+
20
50
  ```ts
21
51
  import { defineConfig } from "vite";
22
52
  import { aiMockPlugin } from "vite-plugin-ai-mock";
@@ -25,12 +55,31 @@ export default defineConfig({
25
55
  plugins: [
26
56
  aiMockPlugin({
27
57
  dataDir: "mock/ai",
28
- endpoint: "/api/ai/mock",
58
+ endpoint: "/api/mock/ai", // /api/mock/ai/chat → chat.json
29
59
  }),
30
60
  ],
31
61
  });
32
62
  ```
33
63
 
64
+ **mock/ai/chat.json**
65
+
66
+ ```json
67
+ {
68
+ "chunks": [
69
+ { "id": "1", "data": { "type": "start" } },
70
+ { "id": "2", "data": { "type": "text-delta", "delta": "Hello" } },
71
+ { "id": "3", "data": { "type": "text-delta", "delta": " World!" } },
72
+ { "id": "4", "data": { "type": "finish" } }
73
+ ]
74
+ }
75
+ ```
76
+
77
+ </td>
78
+ </tr>
79
+ </table>
80
+
81
+ > 💡 查看完整示例:[examples](https://github.com/quanzhiyuan/vite-plugin-ai-mock/tree/main/examples)(包含 Ant Design X、Assistant UI、Lobe Chat 等集成示例)
82
+
34
83
  ## 场景(11 种)
35
84
 
36
85
  1. 正常完成(默认)
@@ -59,12 +108,12 @@ export default defineConfig({
59
108
  直接在请求 URL 上附加参数,适合临时调试单个接口:
60
109
 
61
110
  ```
62
- /api/ai/mock/default?scenario=jitter
63
- /api/ai/mock/default?firstChunkDelayMs=1000&errorAt=3
111
+ /api/mock/ai/default?scenario=jitter
112
+ /api/mock/ai/default?firstChunkDelayMs=1000&errorAt=3
64
113
  ```
65
114
 
66
115
  ```ts
67
- const response = await fetch("/api/ai/mock/default?firstChunkDelayMs=4800", {
116
+ const response = await fetch("/api/mock/ai/default?firstChunkDelayMs=4800", {
68
117
  method: "POST",
69
118
  headers: {
70
119
  "Content-Type": "application/json",
@@ -121,16 +170,16 @@ aiMockPlugin({
121
170
 
122
171
  `data` 字段可以完整模拟真实 API 的响应结构。npm 包内置了以下示例文件(位于 `mock/ai/`),可直接复制到项目中使用:
123
172
 
124
- | 文件 | 提供商 |
125
- | --- | --- |
126
- | `mock/ai/openai.json` | OpenAI / 兼容格式 |
127
- | `mock/ai/claude.json` | Anthropic Claude |
128
- | `mock/ai/gemini.json` | Google Gemini |
129
- | `mock/ai/deepseek.json` | DeepSeek |
173
+ | 文件 | 提供商 |
174
+ | -------------------------------- | ----------------- |
175
+ | `mock/ai/openai.json` | OpenAI / 兼容格式 |
176
+ | `mock/ai/claude.json` | Anthropic Claude |
177
+ | `mock/ai/gemini.json` | Google Gemini |
178
+ | `mock/ai/deepseek.json` | DeepSeek |
130
179
  | `mock/ai/deepseek-reasoner.json` | DeepSeek Reasoner |
131
- | `mock/ai/qwen.json` | 通义千问(阿里) |
132
- | `mock/ai/qwen-thinking.json` | 通义千问 Thinking |
133
- | `mock/ai/doubao.json` | 豆包(字节跳动) |
180
+ | `mock/ai/qwen.json` | 通义千问(阿里) |
181
+ | `mock/ai/qwen-thinking.json` | 通义千问 Thinking |
182
+ | `mock/ai/doubao.json` | 豆包(字节跳动) |
134
183
 
135
184
  **OpenAI / 兼容格式**(`openai.json`)——最后一条 `data` 为字符串 `"[DONE]"`:
136
185
 
@@ -179,6 +228,58 @@ aiMockPlugin({
179
228
  }
180
229
  ```
181
230
 
231
+ **AI SDK `useChat`**——兼容 `@ai-sdk/react` 的 `useChat` hook:
232
+
233
+ ```json
234
+ {
235
+ "chunks": [
236
+ { "id": "1", "event": "message", "data": { "type": "start" } },
237
+ {
238
+ "id": "2",
239
+ "event": "message",
240
+ "data": { "type": "text-start", "id": "t1" }
241
+ },
242
+ {
243
+ "id": "3",
244
+ "event": "message",
245
+ "data": { "type": "text-delta", "id": "t1", "delta": "Hello" }
246
+ },
247
+ {
248
+ "id": "4",
249
+ "event": "message",
250
+ "data": { "type": "text-delta", "id": "t1", "delta": ", world!" }
251
+ },
252
+ {
253
+ "id": "5",
254
+ "event": "message",
255
+ "data": { "type": "text-end", "id": "t1" }
256
+ },
257
+ {
258
+ "id": "6",
259
+ "event": "message",
260
+ "data": { "type": "finish", "finishReason": "stop" }
261
+ }
262
+ ]
263
+ }
264
+ ```
265
+
266
+ 配合 `useChat` 使用:
267
+
268
+ ```tsx
269
+ import { useChat } from "@ai-sdk/react";
270
+ import { DefaultChatTransport } from "ai";
271
+
272
+ const { messages, sendMessage, status } = useChat({
273
+ transport: new DefaultChatTransport({
274
+ api: "/api/mock/ai/chat",
275
+ headers: {
276
+ "Content-Type": "application/json",
277
+ Accept: "text/event-stream",
278
+ },
279
+ }),
280
+ });
281
+ ```
282
+
182
283
  ## 接口地址
183
284
 
184
285
  `endpoint` 支持 `string`、`RegExp` 或 `(string | RegExp)[]`。
@@ -191,10 +292,10 @@ aiMockPlugin({
191
292
 
192
293
  ```ts
193
294
  // string(默认)
194
- endpoint: "/api/ai/mock";
195
- // /api/ai/mock → file = "default"
196
- // /api/ai/mock/chat → file = "chat"
197
- // /api/ai/mock/deepseek → file = "deepseek"
295
+ endpoint: "/api/mock/ai";
296
+ // /api/mock/ai → file = "default"
297
+ // /api/mock/ai/chat → file = "chat"
298
+ // /api/mock/ai/deepseek → file = "deepseek"
198
299
 
199
300
  // RegExp
200
301
  endpoint: /^\/api\/ai\/.*/;
@@ -204,26 +305,26 @@ endpoint: /^\/api\/ai\/.*/;
204
305
  endpoint: ["/api/chat", /^\/v2\/ai\/.*/];
205
306
  ```
206
307
 
207
- - `/api/ai/mock`
208
- - `/api/ai/mock/<file>`
308
+ - `/api/mock/ai`
309
+ - `/api/mock/ai/<file>`
209
310
  - `?file=<file>`
210
311
 
211
312
  ## 测试
212
313
 
213
314
  ```bash
214
- npm test
315
+ pnpm test
215
316
  ```
216
317
 
217
318
  ## 构建
218
319
 
219
320
  ```bash
220
- npm run build
321
+ pnpm build
221
322
  ```
222
323
 
223
324
  ## 发布
224
325
 
225
326
  ```bash
226
- npm run release:npm
327
+ pnpm release:npm
227
328
  ```
228
329
 
229
330
  `prepublishOnly` 会自动执行构建、测试和类型检查。
package/dist/index.cjs CHANGED
@@ -154,7 +154,10 @@ function applyChunkMutations(chunks, options) {
154
154
  }
155
155
  if (options.duplicateAt > 0 && options.duplicateAt <= result.length) {
156
156
  const index = options.duplicateAt - 1;
157
- result.splice(index + 1, 0, { ...result[index], id: `${result[index].id}-dup` });
157
+ result.splice(index + 1, 0, {
158
+ ...result[index],
159
+ id: `${result[index].id}-dup`
160
+ });
158
161
  }
159
162
  return result;
160
163
  }
@@ -166,7 +169,8 @@ function isSseRequest(req, reqUrl) {
166
169
  function writeSseEvent(res, options) {
167
170
  if (options.id) res.write(`id: ${options.id}
168
171
  `);
169
- if (options.event && options.event !== "message") res.write(`event: ${options.event}
172
+ if (options.event && options.event !== "message")
173
+ res.write(`event: ${options.event}
170
174
  `);
171
175
  const payload = typeof options.data === "string" ? options.data : JSON.stringify(options.data ?? null);
172
176
  const lines = payload.split("\n");
@@ -186,7 +190,8 @@ function matchEndpoint(pathname, endpoint) {
186
190
  }
187
191
  if (typeof endpoint === "string") {
188
192
  if (pathname === endpoint) return { fileFromPath: "" };
189
- if (pathname.startsWith(`${endpoint}/`)) return { fileFromPath: pathname.slice(endpoint.length + 1) };
193
+ if (pathname.startsWith(`${endpoint}/`))
194
+ return { fileFromPath: pathname.slice(endpoint.length + 1) };
190
195
  return null;
191
196
  }
192
197
  return endpoint.test(pathname) ? { fileFromPath: "" } : null;
@@ -202,22 +207,45 @@ function aiMockPlugin(config) {
202
207
  if (!req.url) return next();
203
208
  const reqUrl = new URL(req.url, "http://localhost");
204
209
  const matched = matchEndpoint(reqUrl.pathname, endpoint);
210
+ if (req.url.startsWith("/api")) {
211
+ console.log("[aiMockPlugin] Request:", req.method, req.url);
212
+ console.log("[aiMockPlugin] Configured endpoint:", endpoint);
213
+ console.log("[aiMockPlugin] Matched:", matched);
214
+ }
205
215
  if (matched === null) return next();
206
216
  const fileFromPath = matched.fileFromPath;
207
217
  const lastEventIdHeader = typeof req.headers["last-event-id"] === "string" ? req.headers["last-event-id"] : void 0;
208
- const options = parseScenarioOptions(reqUrl, lastEventIdHeader, defaultScenario);
218
+ const options = parseScenarioOptions(
219
+ reqUrl,
220
+ lastEventIdHeader,
221
+ defaultScenario
222
+ );
209
223
  if (fileFromPath) options.file = fileFromPath;
210
224
  try {
211
225
  if (options.httpErrorStatus >= 400) {
226
+ console.log(
227
+ "[aiMockPlugin] Returning HTTP error:",
228
+ options.httpErrorStatus
229
+ );
212
230
  res.statusCode = options.httpErrorStatus;
213
231
  res.setHeader("Content-Type", "application/json; charset=utf-8");
214
- res.end(JSON.stringify({ error: "http_error", status: options.httpErrorStatus }));
232
+ res.end(
233
+ JSON.stringify({
234
+ error: "http_error",
235
+ status: options.httpErrorStatus
236
+ })
237
+ );
215
238
  return;
216
239
  }
217
240
  const filePath = resolveDataFile(dataDir, options.file);
241
+ console.log("[aiMockPlugin] Resolving mock file:", filePath);
242
+ if (!import_node_fs.default.existsSync(filePath)) {
243
+ console.error("[aiMockPlugin] Mock file not found:", filePath);
244
+ }
218
245
  const raw = readJsonFile(filePath);
219
246
  const chunks = applyChunkMutations(normalizeChunks(raw), options);
220
247
  if (!isSseRequest(req, reqUrl)) {
248
+ console.log("[aiMockPlugin] Handling as JSON response");
221
249
  res.statusCode = 200;
222
250
  res.setHeader("Content-Type", "application/json; charset=utf-8");
223
251
  res.end(
@@ -231,6 +259,7 @@ function aiMockPlugin(config) {
231
259
  );
232
260
  return;
233
261
  }
262
+ console.log("[aiMockPlugin] Handling as SSE stream");
234
263
  res.statusCode = 200;
235
264
  res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
236
265
  res.setHeader("Cache-Control", "no-cache, no-transform");
@@ -317,7 +346,9 @@ function aiMockPlugin(config) {
317
346
  res.end();
318
347
  return;
319
348
  }
320
- const interval = typeof nextChunk.delayMs === "number" ? nextChunk.delayMs : options.minIntervalMs + Math.floor(Math.random() * (options.maxIntervalMs - options.minIntervalMs + 1));
349
+ const interval = typeof nextChunk.delayMs === "number" ? nextChunk.delayMs : options.minIntervalMs + Math.floor(
350
+ Math.random() * (options.maxIntervalMs - options.minIntervalMs + 1)
351
+ );
321
352
  schedule(() => writeChunk(nextChunk, index + 1), interval);
322
353
  };
323
354
  if (chunks.length === 0) {
@@ -1 +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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,qBAAe;AACf,uBAAiB;AAoDjB,IAAM,eAAe;AAErB,IAAM,mBAAmB;AAAA,EACvB,QAAQ,CAAC;AAAA,EACT,eAAe,EAAE,mBAAmB,KAAK;AAAA,EACzC,QAAQ,EAAE,eAAe,IAAI,eAAe,KAAK;AAAA,EACjD,YAAY,EAAE,cAAc,EAAE;AAAA,EAC9B,SAAS,EAAE,YAAY,GAAG,SAAS,IAAO;AAAA,EAC1C,OAAO,EAAE,SAAS,GAAG,cAAc,aAAa;AAAA,EAChD,WAAW,EAAE,aAAa,EAAE;AAAA,EAC5B,WAAW,EAAE,aAAa,EAAE;AAAA,EAC5B,gBAAgB,EAAE,YAAY,KAAK;AAAA,EACnC,WAAW,EAAE,WAAW,KAAK;AAAA,EAC7B,WAAW,EAAE,aAAa,KAAK;AACjC;AAQA,SAAS,iBAAiB,OAAsB,UAA0B;AACxE,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,IAAI,OAAO,SAAS,OAAO,EAAE;AACnC,SAAO,OAAO,SAAS,CAAC,KAAK,KAAK,IAAI,IAAI;AAC5C;AAEA,SAAS,aAAa,UAA2B;AAC/C,QAAM,UAAU,eAAAA,QAAG,aAAa,UAAU,OAAO;AACjD,SAAO,KAAK,MAAM,OAAO;AAC3B;AAEA,SAAS,aAAa,MAAsB;AAC1C,SAAO,KAAK,QAAQ,oBAAoB,EAAE;AAC5C;AAEA,SAAS,gBAAgB,SAAiB,UAA0B;AAClE,QAAM,WAAW,aAAa,QAAQ,KAAK;AAC3C,QAAM,kBAAkB,iBAAAC,QAAK,QAAQ,QAAQ,IAAI,GAAG,OAAO;AAC3D,QAAM,YAAY,SAAS,SAAS,OAAO,IACvC,iBAAAA,QAAK,KAAK,iBAAiB,QAAQ,IACnC,iBAAAA,QAAK,KAAK,iBAAiB,GAAG,QAAQ,OAAO;AAEjD,MAAI,CAAC,UAAU,WAAW,eAAe,GAAG;AAC1C,UAAM,IAAI,MAAM,yBAAyB;AAAA,EAC3C;AAEA,MAAI,CAAC,eAAAD,QAAG,WAAW,SAAS,GAAG;AAC7B,UAAM,IAAI,MAAM,6BAA6B,iBAAAC,QAAK,SAAS,SAAS,CAAC,EAAE;AAAA,EACzE;AAEA,SAAO;AACT;AAEA,SAAS,gBAAgB,KAAiC;AACxD,QAAM,SAAS,MAAM,QAAQ,GAAG,IAC5B,MACA,OAAO,QAAQ,YAAY,QAAQ,QAAQ,YAAY,MACpD,IAA4B,SAC7B,CAAC,GAAG;AAEV,MAAI,CAAC,MAAM,QAAQ,MAAM,EAAG,QAAO,CAAC;AAEpC,SAAO,OAAO,IAAI,CAAC,MAAM,UAAU;AACjC,QAAI,OAAO,SAAS,YAAY,SAAS,MAAM;AAC7C,YAAM,QAAQ;AACd,aAAO;AAAA,QACL,IAAI,OAAO,MAAM,MAAM,QAAQ,CAAC;AAAA,QAChC,OAAO,MAAM,SAAS;AAAA,QACtB,MAAM,MAAM,QAAQ;AAAA,QACpB,SAAS,MAAM;AAAA,MACjB;AAAA,IACF;AAEA,WAAO;AAAA,MACL,IAAI,OAAO,QAAQ,CAAC;AAAA,MACpB,OAAO;AAAA,MACP,MAAM;AAAA,IACR;AAAA,EACF,CAAC;AACH;AAEA,SAAS,qBACP,QACA,mBACA,iBACiB;AACjB,QAAM,SAAS,OAAO;AAGtB,QAAM,aAAc,OAAO,IAAI,UAAU,KACpC,iBAAiB;AACtB,QAAM,SAAS,aAAa,iBAAiB,UAAU,KAAK,CAAC,IAAI,CAAC;AAGlE,QAAM,WAAW,CACf,WACA,aAC8B;AAC9B,UAAM,aAAa,OAAO,IAAI,OAAO,SAAS,CAAC;AAC/C,QAAI,eAAe,MAAM;AACvB,aAAO,OAAO,aAAa,WAAW,iBAAiB,YAAY,QAAQ,IAAI;AAAA,IACjF;AACA,QAAI,mBAAmB,aAAa,iBAAiB;AACnD,aAAO,gBAAgB,SAAwC,KAAK;AAAA,IACtE;AACA,UAAM,cAAe,OAAmC,OAAO,SAAS,CAAC;AACzE,QAAI,gBAAgB,QAAW;AAC7B,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT;AAEA,QAAM,oBAAoB,SAAS,qBAAqB,CAAC;AACzD,MAAI,gBAAgB,SAAS,iBAAiB,GAAG;AACjD,MAAI,gBAAgB,SAAS,iBAAiB,GAAG;AAEjD,SAAO;AAAA,IACL,MAAM,OAAO,IAAI,MAAM,KAAK;AAAA,IAC5B;AAAA,IACA,eAAe,KAAK,IAAI,eAAe,aAAa;AAAA,IACpD,eAAe,KAAK,IAAI,eAAe,aAAa;AAAA,IACpD,cAAc,SAAS,gBAAgB,EAAE;AAAA,IACzC,YAAY,SAAS,cAAc,EAAE;AAAA,IACrC,SAAS,SAAS,WAAW,GAAM;AAAA,IACnC,iBAAiB,iBAAiB,OAAO,IAAI,iBAAiB,GAAG,CAAC;AAAA,IAClE,SAAS,SAAS,WAAW,EAAE;AAAA,IAC/B,cAAe,SAAS,gBAAgB,YAAY;AAAA,IACpD,aAAa,SAAS,eAAe,EAAE;AAAA,IACvC,aAAa,SAAS,eAAe,EAAE;AAAA,IACvC,YACE,OAAO,IAAI,YAAY,MAAM,UAC7B,QAAQ,iBAAiB,UAAU,KACnC,QAAS,OAAoC,UAAU;AAAA,IACzD,aAAa,SAAS,eAAe,CAAC;AAAA,IACtC,aAAa,OAAO,IAAI,aAAa,MAAM;AAAA,IAC3C,WACE,OAAO,IAAI,WAAW,MAAM,UAC5B,QAAQ,iBAAiB,SAAS,KAClC,QAAS,OAAmC,SAAS;AAAA,IACvD,aAAa,OAAO,IAAI,aAAa,KAAK,qBAAqB;AAAA,EACjE;AACF;AAEA,SAAS,eAAe,QAA2B,aAAoC;AACrF,MAAI,CAAC,YAAa,QAAO;AACzB,QAAM,WAAW,OAAO,UAAU,CAAC,UAAU,MAAM,OAAO,WAAW;AACrE,SAAO,YAAY,IAAI,WAAW,IAAI;AACxC;AAEA,SAAS,oBAAoB,QAA2B,SAA6C;AACnG,MAAI,SAAS,OAAO,IAAI,CAAC,UAAU,EAAE,GAAG,KAAK,EAAE;AAE/C,MAAI,QAAQ,aAAa,QAAQ,aAAa;AAC5C,UAAM,aAAa,eAAe,QAAQ,QAAQ,WAAW;AAC7D,aAAS,OAAO,MAAM,UAAU;AAAA,EAClC;AAEA,MAAI,QAAQ,cAAc,OAAO,SAAS,GAAG;AAC3C,UAAM,UAAU,CAAC,GAAG,MAAM;AAC1B,UAAM,OAAO,QAAQ,CAAC;AACtB,YAAQ,CAAC,IAAI,QAAQ,CAAC;AACtB,YAAQ,CAAC,IAAI;AACb,aAAS;AAAA,EACX;AAEA,MAAI,QAAQ,cAAc,KAAK,QAAQ,eAAe,OAAO,QAAQ;AACnE,UAAM,QAAQ,QAAQ,cAAc;AACpC,WAAO,OAAO,QAAQ,GAAG,GAAG,EAAE,GAAG,OAAO,KAAK,GAAG,IAAI,GAAG,OAAO,KAAK,EAAE,EAAE,OAAO,CAAC;AAAA,EACjF;AAEA,SAAO;AACT;AAEA,SAAS,aAAa,KAAiE,QAAsB;AAC3G,QAAM,SAAS,OAAO,IAAI,QAAQ,UAAU,EAAE;AAC9C,QAAM,YAAY,OAAO,aAAa,IAAI,WAAW;AACrD,SAAO,OAAO,SAAS,mBAAmB,KAAK,cAAc;AAC/D;AAEA,SAAS,cACP,KAGA,SACM;AACN,MAAI,QAAQ,GAAI,KAAI,MAAM,OAAO,QAAQ,EAAE;AAAA,CAAI;AAC/C,MAAI,QAAQ,SAAS,QAAQ,UAAU,UAAW,KAAI,MAAM,UAAU,QAAQ,KAAK;AAAA,CAAI;AAEvF,QAAM,UAAU,OAAO,QAAQ,SAAS,WAAW,QAAQ,OAAO,KAAK,UAAU,QAAQ,QAAQ,IAAI;AACrG,QAAM,QAAQ,QAAQ,MAAM,IAAI;AAChC,aAAW,QAAQ,OAAO;AACxB,QAAI,MAAM,SAAS,IAAI;AAAA,CAAI;AAAA,EAC7B;AACA,MAAI,MAAM,IAAI;AAChB;AAMA,SAAS,cAAc,UAAkB,UAAuD;AAC9F,MAAI,MAAM,QAAQ,QAAQ,GAAG;AAC3B,eAAW,QAAQ,UAAU;AAC3B,YAAM,SAAS,cAAc,UAAU,IAAI;AAC3C,UAAI,WAAW,KAAM,QAAO;AAAA,IAC9B;AACA,WAAO;AAAA,EACT;AACA,MAAI,OAAO,aAAa,UAAU;AAChC,QAAI,aAAa,SAAU,QAAO,EAAE,cAAc,GAAG;AACrD,QAAI,SAAS,WAAW,GAAG,QAAQ,GAAG,EAAG,QAAO,EAAE,cAAc,SAAS,MAAM,SAAS,SAAS,CAAC,EAAE;AACpG,WAAO;AAAA,EACT;AAEA,SAAO,SAAS,KAAK,QAAQ,IAAI,EAAE,cAAc,GAAG,IAAI;AAC1D;AAEO,SAAS,aAAa,QAAsC;AACjE,QAAM,UAAU,QAAQ,WAAW;AACnC,QAAM,WAA4B,QAAQ,YAAY;AACtD,QAAM,kBAAkB,QAAQ;AAEhC,SAAO;AAAA,IACL,MAAM;AAAA,IACN,gBAAgB,QAAQ;AACtB,aAAO,YAAY,IAAI,CAAC,KAAK,KAAK,SAAS;AACzC,YAAI,CAAC,IAAI,IAAK,QAAO,KAAK;AAC1B,cAAM,SAAS,IAAI,IAAI,IAAI,KAAK,kBAAkB;AAClD,cAAM,UAAU,cAAc,OAAO,UAAU,QAAQ;AACvD,YAAI,YAAY,KAAM,QAAO,KAAK;AAClC,cAAM,eAAe,QAAQ;AAE7B,cAAM,oBACJ,OAAO,IAAI,QAAQ,eAAe,MAAM,WAAW,IAAI,QAAQ,eAAe,IAAI;AAEpF,cAAM,UAAU,qBAAqB,QAAQ,mBAAmB,eAAe;AAC/E,YAAI,aAAc,SAAQ,OAAO;AAEjC,YAAI;AACF,cAAI,QAAQ,mBAAmB,KAAK;AAClC,gBAAI,aAAa,QAAQ;AACzB,gBAAI,UAAU,gBAAgB,iCAAiC;AAC/D,gBAAI,IAAI,KAAK,UAAU,EAAE,OAAO,cAAc,QAAQ,QAAQ,gBAAgB,CAAC,CAAC;AAChF;AAAA,UACF;AAEA,gBAAM,WAAW,gBAAgB,SAAS,QAAQ,IAAI;AACtD,gBAAM,MAAM,aAAa,QAAQ;AACjC,gBAAM,SAAS,oBAAoB,gBAAgB,GAAG,GAAG,OAAO;AAEhE,cAAI,CAAC,aAAa,KAAK,MAAM,GAAG;AAC9B,gBAAI,aAAa;AACjB,gBAAI,UAAU,gBAAgB,iCAAiC;AAC/D,gBAAI;AAAA,cACF,KAAK,UAAU;AAAA,gBACb,MAAM;AAAA,gBACN,MAAM,iBAAAA,QAAK,SAAS,QAAQ;AAAA,gBAC5B,OAAO,OAAO;AAAA,gBACd;AAAA,gBACA;AAAA,cACF,CAAC;AAAA,YACH;AACA;AAAA,UACF;AAEA,cAAI,aAAa;AACjB,cAAI,UAAU,gBAAgB,kCAAkC;AAChE,cAAI,UAAU,iBAAiB,wBAAwB;AACvD,cAAI,UAAU,cAAc,YAAY;AACxC,cAAI,UAAU,qBAAqB,IAAI;AACvC,cAAI,kBAAkB,OAAO,OAAO,IAAI,iBAAiB,YAAY;AACnE,gBAAI,aAAa;AAAA,UACnB;AAEA,cAAI,SAAS;AACb,cAAI,iBAAwC;AAC5C,gBAAM,gBAAgB,oBAAI,IAAoB;AAE9C,gBAAM,UAAU,MAAM;AACpB,gBAAI,OAAQ;AACZ,qBAAS;AACT,gBAAI,eAAgB,eAAc,cAAc;AAChD,uBAAW,SAAS,cAAe,cAAa,KAAK;AACrD,0BAAc,MAAM;AAAA,UACtB;AAEA,cAAI,GAAG,SAAS,OAAO;AAEvB,cAAI,QAAQ,cAAc,GAAG;AAC3B,6BAAiB,YAAY,MAAM;AACjC,kBAAI,OAAQ;AACZ,kBAAI,MAAM,UAAU,KAAK,IAAI,CAAC;AAAA;AAAA,CAAM;AAAA,YACtC,GAAG,QAAQ,WAAW;AAAA,UACxB;AAEA,gBAAM,WAAW,CAAC,MAAkB,UAAkB;AACpD,kBAAM,QAAQ,WAAW,MAAM;AAC7B,4BAAc,OAAO,KAAK;AAC1B,mBAAK;AAAA,YACP,GAAG,KAAK;AACR,0BAAc,IAAI,KAAK;AAAA,UACzB;AAEA,gBAAM,aAAa,CAAC,OAAwB,UAAkB;AAC5D,gBAAI,OAAQ;AACZ,kBAAM,UAAU,QAAQ;AAExB,gBAAI,QAAQ,iBAAiB,SAAS;AACpC,sBAAQ;AACR,kBAAI,aAAa,OAAO,OAAO,IAAI,YAAY,YAAY;AACzD,oBAAI,QAAQ;AACZ;AAAA,cACF;AACA,kBAAI,IAAI;AACR;AAAA,YACF;AAEA,gBAAI,QAAQ,YAAY,SAAS;AAC/B,4BAAc,KAAK;AAAA,gBACjB,IAAI,MAAM;AAAA,gBACV,OAAO;AAAA,gBACP,MAAM,EAAE,SAAS,QAAQ,cAAc,IAAI,QAAQ;AAAA,cACrD,CAAC;AACD,sBAAQ;AACR,kBAAI,IAAI;AACR;AAAA,YACF;AAEA,gBAAI,QAAQ,gBAAgB,SAAS;AACnC,kBAAI,MAAM,OAAO,MAAM,EAAE;AAAA,CAAI;AAC7B,kBAAI,MAAM,kBAAkB;AAC5B,kBAAI,MAAM,8BAA8B;AAAA,YAC1C,OAAO;AACL,4BAAc,KAAK;AAAA,gBACjB,IAAI,MAAM;AAAA,gBACV,OAAO,MAAM;AAAA,gBACb,MAAM,MAAM;AAAA,cACd,CAAC;AAAA,YACH;AAEA,gBAAI,QAAQ,eAAe,SAAS;AAClC,uBAAS,MAAM;AACb,oBAAI,CAAC,QAAQ;AACX,0BAAQ;AACR,sBAAI,IAAI;AAAA,gBACV;AAAA,cACF,GAAG,QAAQ,OAAO;AAClB;AAAA,YACF;AAEA,kBAAM,YAAY,OAAO,QAAQ,CAAC;AAClC,gBAAI,CAAC,WAAW;AACd,kBAAI,QAAQ,aAAa;AACvB,8BAAc,KAAK,EAAE,OAAO,QAAQ,MAAM,EAAE,MAAM,KAAK,EAAE,CAAC;AAAA,cAC5D;AACA,sBAAQ;AACR,kBAAI,IAAI;AACR;AAAA,YACF;AAEA,kBAAM,WACJ,OAAO,UAAU,YAAY,WACzB,UAAU,UACV,QAAQ,gBAAgB,KAAK,MAAM,KAAK,OAAO,KAAK,QAAQ,gBAAgB,QAAQ,gBAAgB,EAAE;AAE5G,qBAAS,MAAM,WAAW,WAAW,QAAQ,CAAC,GAAG,QAAQ;AAAA,UAC3D;AAEA,cAAI,OAAO,WAAW,GAAG;AACvB,gBAAI,QAAQ,aAAa;AACvB,4BAAc,KAAK,EAAE,OAAO,QAAQ,MAAM,EAAE,MAAM,KAAK,EAAE,CAAC;AAAA,YAC5D;AACA,oBAAQ;AACR,gBAAI,IAAI;AACR;AAAA,UACF;AAEA,mBAAS,MAAM,WAAW,OAAO,CAAC,GAAG,CAAC,GAAG,QAAQ,iBAAiB;AAAA,QACpE,SAAS,OAAO;AACd,cAAI,aAAa;AACjB,cAAI,UAAU,gBAAgB,iCAAiC;AAC/D,cAAI;AAAA,YACF,KAAK,UAAU;AAAA,cACb,OAAO;AAAA,cACP,SAAS,iBAAiB,QAAQ,MAAM,UAAU;AAAA,YACpD,CAAC;AAAA,UACH;AAAA,QACF;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF;AACF;","names":["fs","path"]}
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<\n Omit<ScenarioOptions, \"file\" | \"lastEventId\" | \"includeDone\">\n> {\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 =\n (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\"\n ? clampPositiveInt(paramValue, fallback)\n : paramValue;\n }\n if (defaultScenario && paramName in defaultScenario) {\n return (\n defaultScenario[paramName as keyof DefaultScenarioConfig] ?? fallback\n );\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(\n chunks: NormalizedChunk[],\n lastEventId: string | null,\n): 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(\n chunks: NormalizedChunk[],\n options: ScenarioOptions,\n): 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, {\n ...result[index],\n id: `${result[index].id}-dup`,\n });\n }\n\n return result;\n}\n\nfunction isSseRequest(\n req: { headers: Record<string, string | string[] | undefined> },\n reqUrl: URL,\n): 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\")\n res.write(`event: ${options.event}\\n`);\n\n const payload =\n typeof options.data === \"string\"\n ? options.data\n : 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(\n pathname: string,\n endpoint: EndpointPattern,\n): 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}/`))\n 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\n if (req.url.startsWith(\"/api\")) {\n console.log(\"[aiMockPlugin] Request:\", req.method, req.url);\n console.log(\"[aiMockPlugin] Configured endpoint:\", endpoint);\n console.log(\"[aiMockPlugin] Matched:\", matched);\n }\n\n if (matched === null) return next();\n const fileFromPath = matched.fileFromPath;\n\n const lastEventIdHeader =\n typeof req.headers[\"last-event-id\"] === \"string\"\n ? req.headers[\"last-event-id\"]\n : undefined;\n\n const options = parseScenarioOptions(\n reqUrl,\n lastEventIdHeader,\n defaultScenario,\n );\n if (fileFromPath) options.file = fileFromPath;\n\n try {\n if (options.httpErrorStatus >= 400) {\n console.log(\n \"[aiMockPlugin] Returning HTTP error:\",\n options.httpErrorStatus,\n );\n res.statusCode = options.httpErrorStatus;\n res.setHeader(\"Content-Type\", \"application/json; charset=utf-8\");\n res.end(\n JSON.stringify({\n error: \"http_error\",\n status: options.httpErrorStatus,\n }),\n );\n return;\n }\n\n const filePath = resolveDataFile(dataDir, options.file);\n console.log(\"[aiMockPlugin] Resolving mock file:\", filePath);\n\n if (!fs.existsSync(filePath)) {\n console.error(\"[aiMockPlugin] Mock file not found:\", filePath);\n // Let it throw or handle it\n }\n\n const raw = readJsonFile(filePath);\n const chunks = applyChunkMutations(normalizeChunks(raw), options);\n\n if (!isSseRequest(req, reqUrl)) {\n console.log(\"[aiMockPlugin] Handling as JSON response\");\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 console.log(\"[aiMockPlugin] Handling as SSE stream\");\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 +\n Math.floor(\n Math.random() *\n (options.maxIntervalMs - options.minIntervalMs + 1),\n );\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"]}
package/dist/index.d.cts CHANGED
@@ -67,7 +67,7 @@ declare const SCENARIO_PRESETS: {
67
67
  };
68
68
  };
69
69
  type ScenarioName = keyof typeof SCENARIO_PRESETS;
70
- interface DefaultScenarioConfig extends Partial<Omit<ScenarioOptions, 'file' | 'lastEventId' | 'includeDone'>> {
70
+ interface DefaultScenarioConfig extends Partial<Omit<ScenarioOptions, "file" | "lastEventId" | "includeDone">> {
71
71
  scenario?: ScenarioName;
72
72
  }
73
73
  declare function aiMockPlugin(config?: AiMockPluginOptions): Plugin;
package/dist/index.d.ts CHANGED
@@ -67,7 +67,7 @@ declare const SCENARIO_PRESETS: {
67
67
  };
68
68
  };
69
69
  type ScenarioName = keyof typeof SCENARIO_PRESETS;
70
- interface DefaultScenarioConfig extends Partial<Omit<ScenarioOptions, 'file' | 'lastEventId' | 'includeDone'>> {
70
+ interface DefaultScenarioConfig extends Partial<Omit<ScenarioOptions, "file" | "lastEventId" | "includeDone">> {
71
71
  scenario?: ScenarioName;
72
72
  }
73
73
  declare function aiMockPlugin(config?: AiMockPluginOptions): Plugin;
package/dist/index.js CHANGED
@@ -120,7 +120,10 @@ function applyChunkMutations(chunks, options) {
120
120
  }
121
121
  if (options.duplicateAt > 0 && options.duplicateAt <= result.length) {
122
122
  const index = options.duplicateAt - 1;
123
- result.splice(index + 1, 0, { ...result[index], id: `${result[index].id}-dup` });
123
+ result.splice(index + 1, 0, {
124
+ ...result[index],
125
+ id: `${result[index].id}-dup`
126
+ });
124
127
  }
125
128
  return result;
126
129
  }
@@ -132,7 +135,8 @@ function isSseRequest(req, reqUrl) {
132
135
  function writeSseEvent(res, options) {
133
136
  if (options.id) res.write(`id: ${options.id}
134
137
  `);
135
- if (options.event && options.event !== "message") res.write(`event: ${options.event}
138
+ if (options.event && options.event !== "message")
139
+ res.write(`event: ${options.event}
136
140
  `);
137
141
  const payload = typeof options.data === "string" ? options.data : JSON.stringify(options.data ?? null);
138
142
  const lines = payload.split("\n");
@@ -152,7 +156,8 @@ function matchEndpoint(pathname, endpoint) {
152
156
  }
153
157
  if (typeof endpoint === "string") {
154
158
  if (pathname === endpoint) return { fileFromPath: "" };
155
- if (pathname.startsWith(`${endpoint}/`)) return { fileFromPath: pathname.slice(endpoint.length + 1) };
159
+ if (pathname.startsWith(`${endpoint}/`))
160
+ return { fileFromPath: pathname.slice(endpoint.length + 1) };
156
161
  return null;
157
162
  }
158
163
  return endpoint.test(pathname) ? { fileFromPath: "" } : null;
@@ -168,22 +173,45 @@ function aiMockPlugin(config) {
168
173
  if (!req.url) return next();
169
174
  const reqUrl = new URL(req.url, "http://localhost");
170
175
  const matched = matchEndpoint(reqUrl.pathname, endpoint);
176
+ if (req.url.startsWith("/api")) {
177
+ console.log("[aiMockPlugin] Request:", req.method, req.url);
178
+ console.log("[aiMockPlugin] Configured endpoint:", endpoint);
179
+ console.log("[aiMockPlugin] Matched:", matched);
180
+ }
171
181
  if (matched === null) return next();
172
182
  const fileFromPath = matched.fileFromPath;
173
183
  const lastEventIdHeader = typeof req.headers["last-event-id"] === "string" ? req.headers["last-event-id"] : void 0;
174
- const options = parseScenarioOptions(reqUrl, lastEventIdHeader, defaultScenario);
184
+ const options = parseScenarioOptions(
185
+ reqUrl,
186
+ lastEventIdHeader,
187
+ defaultScenario
188
+ );
175
189
  if (fileFromPath) options.file = fileFromPath;
176
190
  try {
177
191
  if (options.httpErrorStatus >= 400) {
192
+ console.log(
193
+ "[aiMockPlugin] Returning HTTP error:",
194
+ options.httpErrorStatus
195
+ );
178
196
  res.statusCode = options.httpErrorStatus;
179
197
  res.setHeader("Content-Type", "application/json; charset=utf-8");
180
- res.end(JSON.stringify({ error: "http_error", status: options.httpErrorStatus }));
198
+ res.end(
199
+ JSON.stringify({
200
+ error: "http_error",
201
+ status: options.httpErrorStatus
202
+ })
203
+ );
181
204
  return;
182
205
  }
183
206
  const filePath = resolveDataFile(dataDir, options.file);
207
+ console.log("[aiMockPlugin] Resolving mock file:", filePath);
208
+ if (!fs.existsSync(filePath)) {
209
+ console.error("[aiMockPlugin] Mock file not found:", filePath);
210
+ }
184
211
  const raw = readJsonFile(filePath);
185
212
  const chunks = applyChunkMutations(normalizeChunks(raw), options);
186
213
  if (!isSseRequest(req, reqUrl)) {
214
+ console.log("[aiMockPlugin] Handling as JSON response");
187
215
  res.statusCode = 200;
188
216
  res.setHeader("Content-Type", "application/json; charset=utf-8");
189
217
  res.end(
@@ -197,6 +225,7 @@ function aiMockPlugin(config) {
197
225
  );
198
226
  return;
199
227
  }
228
+ console.log("[aiMockPlugin] Handling as SSE stream");
200
229
  res.statusCode = 200;
201
230
  res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
202
231
  res.setHeader("Cache-Control", "no-cache, no-transform");
@@ -283,7 +312,9 @@ function aiMockPlugin(config) {
283
312
  res.end();
284
313
  return;
285
314
  }
286
- const interval = typeof nextChunk.delayMs === "number" ? nextChunk.delayMs : options.minIntervalMs + Math.floor(Math.random() * (options.maxIntervalMs - options.minIntervalMs + 1));
315
+ const interval = typeof nextChunk.delayMs === "number" ? nextChunk.delayMs : options.minIntervalMs + Math.floor(
316
+ Math.random() * (options.maxIntervalMs - options.minIntervalMs + 1)
317
+ );
287
318
  schedule(() => writeChunk(nextChunk, index + 1), interval);
288
319
  };
289
320
  if (chunks.length === 0) {
package/dist/index.js.map CHANGED
@@ -1 +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":[]}
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<\n Omit<ScenarioOptions, \"file\" | \"lastEventId\" | \"includeDone\">\n> {\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 =\n (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\"\n ? clampPositiveInt(paramValue, fallback)\n : paramValue;\n }\n if (defaultScenario && paramName in defaultScenario) {\n return (\n defaultScenario[paramName as keyof DefaultScenarioConfig] ?? fallback\n );\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(\n chunks: NormalizedChunk[],\n lastEventId: string | null,\n): 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(\n chunks: NormalizedChunk[],\n options: ScenarioOptions,\n): 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, {\n ...result[index],\n id: `${result[index].id}-dup`,\n });\n }\n\n return result;\n}\n\nfunction isSseRequest(\n req: { headers: Record<string, string | string[] | undefined> },\n reqUrl: URL,\n): 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\")\n res.write(`event: ${options.event}\\n`);\n\n const payload =\n typeof options.data === \"string\"\n ? options.data\n : 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(\n pathname: string,\n endpoint: EndpointPattern,\n): 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}/`))\n 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\n if (req.url.startsWith(\"/api\")) {\n console.log(\"[aiMockPlugin] Request:\", req.method, req.url);\n console.log(\"[aiMockPlugin] Configured endpoint:\", endpoint);\n console.log(\"[aiMockPlugin] Matched:\", matched);\n }\n\n if (matched === null) return next();\n const fileFromPath = matched.fileFromPath;\n\n const lastEventIdHeader =\n typeof req.headers[\"last-event-id\"] === \"string\"\n ? req.headers[\"last-event-id\"]\n : undefined;\n\n const options = parseScenarioOptions(\n reqUrl,\n lastEventIdHeader,\n defaultScenario,\n );\n if (fileFromPath) options.file = fileFromPath;\n\n try {\n if (options.httpErrorStatus >= 400) {\n console.log(\n \"[aiMockPlugin] Returning HTTP error:\",\n options.httpErrorStatus,\n );\n res.statusCode = options.httpErrorStatus;\n res.setHeader(\"Content-Type\", \"application/json; charset=utf-8\");\n res.end(\n JSON.stringify({\n error: \"http_error\",\n status: options.httpErrorStatus,\n }),\n );\n return;\n }\n\n const filePath = resolveDataFile(dataDir, options.file);\n console.log(\"[aiMockPlugin] Resolving mock file:\", filePath);\n\n if (!fs.existsSync(filePath)) {\n console.error(\"[aiMockPlugin] Mock file not found:\", filePath);\n // Let it throw or handle it\n }\n\n const raw = readJsonFile(filePath);\n const chunks = applyChunkMutations(normalizeChunks(raw), options);\n\n if (!isSseRequest(req, reqUrl)) {\n console.log(\"[aiMockPlugin] Handling as JSON response\");\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 console.log(\"[aiMockPlugin] Handling as SSE stream\");\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 +\n Math.floor(\n Math.random() *\n (options.maxIntervalMs - options.minIntervalMs + 1),\n );\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":";AAAA,OAAO,QAAQ;AACf,OAAO,UAAU;AAoDjB,IAAM,eAAe;AAErB,IAAM,mBAAmB;AAAA,EACvB,QAAQ,CAAC;AAAA,EACT,eAAe,EAAE,mBAAmB,KAAK;AAAA,EACzC,QAAQ,EAAE,eAAe,IAAI,eAAe,KAAK;AAAA,EACjD,YAAY,EAAE,cAAc,EAAE;AAAA,EAC9B,SAAS,EAAE,YAAY,GAAG,SAAS,IAAO;AAAA,EAC1C,OAAO,EAAE,SAAS,GAAG,cAAc,aAAa;AAAA,EAChD,WAAW,EAAE,aAAa,EAAE;AAAA,EAC5B,WAAW,EAAE,aAAa,EAAE;AAAA,EAC5B,gBAAgB,EAAE,YAAY,KAAK;AAAA,EACnC,WAAW,EAAE,WAAW,KAAK;AAAA,EAC7B,WAAW,EAAE,aAAa,KAAK;AACjC;AAUA,SAAS,iBAAiB,OAAsB,UAA0B;AACxE,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,IAAI,OAAO,SAAS,OAAO,EAAE;AACnC,SAAO,OAAO,SAAS,CAAC,KAAK,KAAK,IAAI,IAAI;AAC5C;AAEA,SAAS,aAAa,UAA2B;AAC/C,QAAM,UAAU,GAAG,aAAa,UAAU,OAAO;AACjD,SAAO,KAAK,MAAM,OAAO;AAC3B;AAEA,SAAS,aAAa,MAAsB;AAC1C,SAAO,KAAK,QAAQ,oBAAoB,EAAE;AAC5C;AAEA,SAAS,gBAAgB,SAAiB,UAA0B;AAClE,QAAM,WAAW,aAAa,QAAQ,KAAK;AAC3C,QAAM,kBAAkB,KAAK,QAAQ,QAAQ,IAAI,GAAG,OAAO;AAC3D,QAAM,YAAY,SAAS,SAAS,OAAO,IACvC,KAAK,KAAK,iBAAiB,QAAQ,IACnC,KAAK,KAAK,iBAAiB,GAAG,QAAQ,OAAO;AAEjD,MAAI,CAAC,UAAU,WAAW,eAAe,GAAG;AAC1C,UAAM,IAAI,MAAM,yBAAyB;AAAA,EAC3C;AAEA,MAAI,CAAC,GAAG,WAAW,SAAS,GAAG;AAC7B,UAAM,IAAI,MAAM,6BAA6B,KAAK,SAAS,SAAS,CAAC,EAAE;AAAA,EACzE;AAEA,SAAO;AACT;AAEA,SAAS,gBAAgB,KAAiC;AACxD,QAAM,SAAS,MAAM,QAAQ,GAAG,IAC5B,MACA,OAAO,QAAQ,YAAY,QAAQ,QAAQ,YAAY,MACpD,IAA4B,SAC7B,CAAC,GAAG;AAEV,MAAI,CAAC,MAAM,QAAQ,MAAM,EAAG,QAAO,CAAC;AAEpC,SAAO,OAAO,IAAI,CAAC,MAAM,UAAU;AACjC,QAAI,OAAO,SAAS,YAAY,SAAS,MAAM;AAC7C,YAAM,QAAQ;AACd,aAAO;AAAA,QACL,IAAI,OAAO,MAAM,MAAM,QAAQ,CAAC;AAAA,QAChC,OAAO,MAAM,SAAS;AAAA,QACtB,MAAM,MAAM,QAAQ;AAAA,QACpB,SAAS,MAAM;AAAA,MACjB;AAAA,IACF;AAEA,WAAO;AAAA,MACL,IAAI,OAAO,QAAQ,CAAC;AAAA,MACpB,OAAO;AAAA,MACP,MAAM;AAAA,IACR;AAAA,EACF,CAAC;AACH;AAEA,SAAS,qBACP,QACA,mBACA,iBACiB;AACjB,QAAM,SAAS,OAAO;AAGtB,QAAM,aACH,OAAO,IAAI,UAAU,KACtB,iBAAiB;AACnB,QAAM,SAAS,aAAc,iBAAiB,UAAU,KAAK,CAAC,IAAK,CAAC;AAGpE,QAAM,WAAW,CACf,WACA,aAC8B;AAC9B,UAAM,aAAa,OAAO,IAAI,OAAO,SAAS,CAAC;AAC/C,QAAI,eAAe,MAAM;AACvB,aAAO,OAAO,aAAa,WACvB,iBAAiB,YAAY,QAAQ,IACrC;AAAA,IACN;AACA,QAAI,mBAAmB,aAAa,iBAAiB;AACnD,aACE,gBAAgB,SAAwC,KAAK;AAAA,IAEjE;AACA,UAAM,cAAe,OAAmC,OAAO,SAAS,CAAC;AACzE,QAAI,gBAAgB,QAAW;AAC7B,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT;AAEA,QAAM,oBAAoB,SAAS,qBAAqB,CAAC;AACzD,MAAI,gBAAgB,SAAS,iBAAiB,GAAG;AACjD,MAAI,gBAAgB,SAAS,iBAAiB,GAAG;AAEjD,SAAO;AAAA,IACL,MAAM,OAAO,IAAI,MAAM,KAAK;AAAA,IAC5B;AAAA,IACA,eAAe,KAAK,IAAI,eAAe,aAAa;AAAA,IACpD,eAAe,KAAK,IAAI,eAAe,aAAa;AAAA,IACpD,cAAc,SAAS,gBAAgB,EAAE;AAAA,IACzC,YAAY,SAAS,cAAc,EAAE;AAAA,IACrC,SAAS,SAAS,WAAW,GAAM;AAAA,IACnC,iBAAiB,iBAAiB,OAAO,IAAI,iBAAiB,GAAG,CAAC;AAAA,IAClE,SAAS,SAAS,WAAW,EAAE;AAAA,IAC/B,cAAc,SAAS,gBAAgB,YAAY;AAAA,IACnD,aAAa,SAAS,eAAe,EAAE;AAAA,IACvC,aAAa,SAAS,eAAe,EAAE;AAAA,IACvC,YACE,OAAO,IAAI,YAAY,MAAM,UAC7B,QAAQ,iBAAiB,UAAU,KACnC,QAAS,OAAoC,UAAU;AAAA,IACzD,aAAa,SAAS,eAAe,CAAC;AAAA,IACtC,aAAa,OAAO,IAAI,aAAa,MAAM;AAAA,IAC3C,WACE,OAAO,IAAI,WAAW,MAAM,UAC5B,QAAQ,iBAAiB,SAAS,KAClC,QAAS,OAAmC,SAAS;AAAA,IACvD,aAAa,OAAO,IAAI,aAAa,KAAK,qBAAqB;AAAA,EACjE;AACF;AAEA,SAAS,eACP,QACA,aACQ;AACR,MAAI,CAAC,YAAa,QAAO;AACzB,QAAM,WAAW,OAAO,UAAU,CAAC,UAAU,MAAM,OAAO,WAAW;AACrE,SAAO,YAAY,IAAI,WAAW,IAAI;AACxC;AAEA,SAAS,oBACP,QACA,SACmB;AACnB,MAAI,SAAS,OAAO,IAAI,CAAC,UAAU,EAAE,GAAG,KAAK,EAAE;AAE/C,MAAI,QAAQ,aAAa,QAAQ,aAAa;AAC5C,UAAM,aAAa,eAAe,QAAQ,QAAQ,WAAW;AAC7D,aAAS,OAAO,MAAM,UAAU;AAAA,EAClC;AAEA,MAAI,QAAQ,cAAc,OAAO,SAAS,GAAG;AAC3C,UAAM,UAAU,CAAC,GAAG,MAAM;AAC1B,UAAM,OAAO,QAAQ,CAAC;AACtB,YAAQ,CAAC,IAAI,QAAQ,CAAC;AACtB,YAAQ,CAAC,IAAI;AACb,aAAS;AAAA,EACX;AAEA,MAAI,QAAQ,cAAc,KAAK,QAAQ,eAAe,OAAO,QAAQ;AACnE,UAAM,QAAQ,QAAQ,cAAc;AACpC,WAAO,OAAO,QAAQ,GAAG,GAAG;AAAA,MAC1B,GAAG,OAAO,KAAK;AAAA,MACf,IAAI,GAAG,OAAO,KAAK,EAAE,EAAE;AAAA,IACzB,CAAC;AAAA,EACH;AAEA,SAAO;AACT;AAEA,SAAS,aACP,KACA,QACS;AACT,QAAM,SAAS,OAAO,IAAI,QAAQ,UAAU,EAAE;AAC9C,QAAM,YAAY,OAAO,aAAa,IAAI,WAAW;AACrD,SAAO,OAAO,SAAS,mBAAmB,KAAK,cAAc;AAC/D;AAEA,SAAS,cACP,KAGA,SACM;AACN,MAAI,QAAQ,GAAI,KAAI,MAAM,OAAO,QAAQ,EAAE;AAAA,CAAI;AAC/C,MAAI,QAAQ,SAAS,QAAQ,UAAU;AACrC,QAAI,MAAM,UAAU,QAAQ,KAAK;AAAA,CAAI;AAEvC,QAAM,UACJ,OAAO,QAAQ,SAAS,WACpB,QAAQ,OACR,KAAK,UAAU,QAAQ,QAAQ,IAAI;AACzC,QAAM,QAAQ,QAAQ,MAAM,IAAI;AAChC,aAAW,QAAQ,OAAO;AACxB,QAAI,MAAM,SAAS,IAAI;AAAA,CAAI;AAAA,EAC7B;AACA,MAAI,MAAM,IAAI;AAChB;AAMA,SAAS,cACP,UACA,UAC4B;AAC5B,MAAI,MAAM,QAAQ,QAAQ,GAAG;AAC3B,eAAW,QAAQ,UAAU;AAC3B,YAAM,SAAS,cAAc,UAAU,IAAI;AAC3C,UAAI,WAAW,KAAM,QAAO;AAAA,IAC9B;AACA,WAAO;AAAA,EACT;AACA,MAAI,OAAO,aAAa,UAAU;AAChC,QAAI,aAAa,SAAU,QAAO,EAAE,cAAc,GAAG;AACrD,QAAI,SAAS,WAAW,GAAG,QAAQ,GAAG;AACpC,aAAO,EAAE,cAAc,SAAS,MAAM,SAAS,SAAS,CAAC,EAAE;AAC7D,WAAO;AAAA,EACT;AAEA,SAAO,SAAS,KAAK,QAAQ,IAAI,EAAE,cAAc,GAAG,IAAI;AAC1D;AAEO,SAAS,aAAa,QAAsC;AACjE,QAAM,UAAU,QAAQ,WAAW;AACnC,QAAM,WAA4B,QAAQ,YAAY;AACtD,QAAM,kBAAkB,QAAQ;AAEhC,SAAO;AAAA,IACL,MAAM;AAAA,IACN,gBAAgB,QAAQ;AACtB,aAAO,YAAY,IAAI,CAAC,KAAK,KAAK,SAAS;AACzC,YAAI,CAAC,IAAI,IAAK,QAAO,KAAK;AAC1B,cAAM,SAAS,IAAI,IAAI,IAAI,KAAK,kBAAkB;AAClD,cAAM,UAAU,cAAc,OAAO,UAAU,QAAQ;AAEvD,YAAI,IAAI,IAAI,WAAW,MAAM,GAAG;AAC9B,kBAAQ,IAAI,2BAA2B,IAAI,QAAQ,IAAI,GAAG;AAC1D,kBAAQ,IAAI,uCAAuC,QAAQ;AAC3D,kBAAQ,IAAI,2BAA2B,OAAO;AAAA,QAChD;AAEA,YAAI,YAAY,KAAM,QAAO,KAAK;AAClC,cAAM,eAAe,QAAQ;AAE7B,cAAM,oBACJ,OAAO,IAAI,QAAQ,eAAe,MAAM,WACpC,IAAI,QAAQ,eAAe,IAC3B;AAEN,cAAM,UAAU;AAAA,UACd;AAAA,UACA;AAAA,UACA;AAAA,QACF;AACA,YAAI,aAAc,SAAQ,OAAO;AAEjC,YAAI;AACF,cAAI,QAAQ,mBAAmB,KAAK;AAClC,oBAAQ;AAAA,cACN;AAAA,cACA,QAAQ;AAAA,YACV;AACA,gBAAI,aAAa,QAAQ;AACzB,gBAAI,UAAU,gBAAgB,iCAAiC;AAC/D,gBAAI;AAAA,cACF,KAAK,UAAU;AAAA,gBACb,OAAO;AAAA,gBACP,QAAQ,QAAQ;AAAA,cAClB,CAAC;AAAA,YACH;AACA;AAAA,UACF;AAEA,gBAAM,WAAW,gBAAgB,SAAS,QAAQ,IAAI;AACtD,kBAAQ,IAAI,uCAAuC,QAAQ;AAE3D,cAAI,CAAC,GAAG,WAAW,QAAQ,GAAG;AAC5B,oBAAQ,MAAM,uCAAuC,QAAQ;AAAA,UAE/D;AAEA,gBAAM,MAAM,aAAa,QAAQ;AACjC,gBAAM,SAAS,oBAAoB,gBAAgB,GAAG,GAAG,OAAO;AAEhE,cAAI,CAAC,aAAa,KAAK,MAAM,GAAG;AAC9B,oBAAQ,IAAI,0CAA0C;AACtD,gBAAI,aAAa;AACjB,gBAAI,UAAU,gBAAgB,iCAAiC;AAC/D,gBAAI;AAAA,cACF,KAAK,UAAU;AAAA,gBACb,MAAM;AAAA,gBACN,MAAM,KAAK,SAAS,QAAQ;AAAA,gBAC5B,OAAO,OAAO;AAAA,gBACd;AAAA,gBACA;AAAA,cACF,CAAC;AAAA,YACH;AACA;AAAA,UACF;AAEA,kBAAQ,IAAI,uCAAuC;AACnD,cAAI,aAAa;AACjB,cAAI,UAAU,gBAAgB,kCAAkC;AAChE,cAAI,UAAU,iBAAiB,wBAAwB;AACvD,cAAI,UAAU,cAAc,YAAY;AACxC,cAAI,UAAU,qBAAqB,IAAI;AACvC,cAAI,kBAAkB,OAAO,OAAO,IAAI,iBAAiB,YAAY;AACnE,gBAAI,aAAa;AAAA,UACnB;AAEA,cAAI,SAAS;AACb,cAAI,iBAAwC;AAC5C,gBAAM,gBAAgB,oBAAI,IAAoB;AAE9C,gBAAM,UAAU,MAAM;AACpB,gBAAI,OAAQ;AACZ,qBAAS;AACT,gBAAI,eAAgB,eAAc,cAAc;AAChD,uBAAW,SAAS,cAAe,cAAa,KAAK;AACrD,0BAAc,MAAM;AAAA,UACtB;AAEA,cAAI,GAAG,SAAS,OAAO;AAEvB,cAAI,QAAQ,cAAc,GAAG;AAC3B,6BAAiB,YAAY,MAAM;AACjC,kBAAI,OAAQ;AACZ,kBAAI,MAAM,UAAU,KAAK,IAAI,CAAC;AAAA;AAAA,CAAM;AAAA,YACtC,GAAG,QAAQ,WAAW;AAAA,UACxB;AAEA,gBAAM,WAAW,CAAC,MAAkB,UAAkB;AACpD,kBAAM,QAAQ,WAAW,MAAM;AAC7B,4BAAc,OAAO,KAAK;AAC1B,mBAAK;AAAA,YACP,GAAG,KAAK;AACR,0BAAc,IAAI,KAAK;AAAA,UACzB;AAEA,gBAAM,aAAa,CAAC,OAAwB,UAAkB;AAC5D,gBAAI,OAAQ;AACZ,kBAAM,UAAU,QAAQ;AAExB,gBAAI,QAAQ,iBAAiB,SAAS;AACpC,sBAAQ;AACR,kBAAI,aAAa,OAAO,OAAO,IAAI,YAAY,YAAY;AACzD,oBAAI,QAAQ;AACZ;AAAA,cACF;AACA,kBAAI,IAAI;AACR;AAAA,YACF;AAEA,gBAAI,QAAQ,YAAY,SAAS;AAC/B,4BAAc,KAAK;AAAA,gBACjB,IAAI,MAAM;AAAA,gBACV,OAAO;AAAA,gBACP,MAAM,EAAE,SAAS,QAAQ,cAAc,IAAI,QAAQ;AAAA,cACrD,CAAC;AACD,sBAAQ;AACR,kBAAI,IAAI;AACR;AAAA,YACF;AAEA,gBAAI,QAAQ,gBAAgB,SAAS;AACnC,kBAAI,MAAM,OAAO,MAAM,EAAE;AAAA,CAAI;AAC7B,kBAAI,MAAM,kBAAkB;AAC5B,kBAAI,MAAM,8BAA8B;AAAA,YAC1C,OAAO;AACL,4BAAc,KAAK;AAAA,gBACjB,IAAI,MAAM;AAAA,gBACV,OAAO,MAAM;AAAA,gBACb,MAAM,MAAM;AAAA,cACd,CAAC;AAAA,YACH;AAEA,gBAAI,QAAQ,eAAe,SAAS;AAClC,uBAAS,MAAM;AACb,oBAAI,CAAC,QAAQ;AACX,0BAAQ;AACR,sBAAI,IAAI;AAAA,gBACV;AAAA,cACF,GAAG,QAAQ,OAAO;AAClB;AAAA,YACF;AAEA,kBAAM,YAAY,OAAO,QAAQ,CAAC;AAClC,gBAAI,CAAC,WAAW;AACd,kBAAI,QAAQ,aAAa;AACvB,8BAAc,KAAK,EAAE,OAAO,QAAQ,MAAM,EAAE,MAAM,KAAK,EAAE,CAAC;AAAA,cAC5D;AACA,sBAAQ;AACR,kBAAI,IAAI;AACR;AAAA,YACF;AAEA,kBAAM,WACJ,OAAO,UAAU,YAAY,WACzB,UAAU,UACV,QAAQ,gBACR,KAAK;AAAA,cACH,KAAK,OAAO,KACT,QAAQ,gBAAgB,QAAQ,gBAAgB;AAAA,YACrD;AAEN,qBAAS,MAAM,WAAW,WAAW,QAAQ,CAAC,GAAG,QAAQ;AAAA,UAC3D;AAEA,cAAI,OAAO,WAAW,GAAG;AACvB,gBAAI,QAAQ,aAAa;AACvB,4BAAc,KAAK,EAAE,OAAO,QAAQ,MAAM,EAAE,MAAM,KAAK,EAAE,CAAC;AAAA,YAC5D;AACA,oBAAQ;AACR,gBAAI,IAAI;AACR;AAAA,UACF;AAEA,mBAAS,MAAM,WAAW,OAAO,CAAC,GAAG,CAAC,GAAG,QAAQ,iBAAiB;AAAA,QACpE,SAAS,OAAO;AACd,cAAI,aAAa;AACjB,cAAI,UAAU,gBAAgB,iCAAiC;AAC/D,cAAI;AAAA,YACF,KAAK,UAAU;AAAA,cACb,OAAO;AAAA,cACP,SAAS,iBAAiB,QAAQ,MAAM,UAAU;AAAA,YACpD,CAAC;AAAA,UACH;AAAA,QACF;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF;AACF;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vite-plugin-ai-mock",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "A Vite plugin for AI streaming mock (SSE/JSON) with configurable scenarios.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -42,15 +42,6 @@
42
42
  "peerDependencies": {
43
43
  "vite": ">=5.0.0"
44
44
  },
45
- "scripts": {
46
- "build": "tsup",
47
- "test": "vitest run",
48
- "test:watch": "vitest",
49
- "typecheck": "tsc --noEmit",
50
- "clean": "rimraf dist",
51
- "prepublishOnly": "npm run clean && npm run build && npm run test && npm run typecheck",
52
- "release:npm": "npm publish --access public"
53
- },
54
45
  "devDependencies": {
55
46
  "@types/node": "^24.10.1",
56
47
  "rimraf": "^6.1.1",
@@ -58,5 +49,13 @@
58
49
  "typescript": "^5.9.3",
59
50
  "vite": "^7.2.4",
60
51
  "vitest": "^3.2.4"
52
+ },
53
+ "scripts": {
54
+ "build": "tsup",
55
+ "test": "vitest run",
56
+ "test:watch": "vitest",
57
+ "typecheck": "tsc --noEmit",
58
+ "clean": "rimraf dist",
59
+ "release:npm": "npm publish --access public"
61
60
  }
62
61
  }