vite-plugin-ai-mock 0.1.4 → 0.1.5

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
@@ -10,7 +10,7 @@
10
10
 
11
11
  A standalone Vite plugin for AI scene mocking. Returns streaming data in JSON format, simulating various AI scenarios.
12
12
 
13
- - Reads mock files from `mock/ai/*.json`
13
+ - Reads mock files from `mock/*.json`
14
14
  - Returns SSE streaming response by default
15
15
  - Use `?transport=json` to get JSON format response
16
16
  - Supports 11 streaming scenarios with request parameters
@@ -37,15 +37,15 @@ project/
37
37
  │ └── default.json
38
38
  ├── src/
39
39
  └── vite.config.ts
40
+ ```
40
41
 
42
+ **Request Example**
41
43
 
42
-
43
-
44
-
45
-
46
-
47
-
48
-
44
+ ```ts
45
+ // SSE streaming (default)
46
+ const res = await fetch("/api/chat");
47
+ // JSON response
48
+ const json = await fetch("/api/chat?transport=json");
49
49
  ```
50
50
 
51
51
  </td>
@@ -60,14 +60,14 @@ import { aiMockPlugin } from "vite-plugin-ai-mock";
60
60
  export default defineConfig({
61
61
  plugins: [
62
62
  aiMockPlugin({
63
- dataDir: "mock/ai",
64
- endpoint: "/api/mock/ai", // /api/mock/ai/chat → chat.json
63
+ dataDir: "mock",
64
+ endpoint: "/api", // /api/chat → chat.json
65
65
  }),
66
66
  ],
67
67
  });
68
68
  ```
69
69
 
70
- **mock/ai/chat.json**
70
+ **mock/chat.json**
71
71
 
72
72
  ```json
73
73
  {
@@ -114,20 +114,20 @@ Scenarios can be configured in two ways, in order of precedence:
114
114
  Append parameters directly to the request URL, useful for debugging a single endpoint:
115
115
 
116
116
  ```
117
- /api/mock/ai/default?scenario=jitter
118
- /api/mock/ai/default?firstChunkDelayMs=1000&errorAt=3
117
+ /api/default?scenario=jitter
118
+ /api/default?firstChunkDelayMs=1000&errorAt=3
119
119
  ```
120
120
 
121
121
  ```ts
122
122
  // Default returns SSE streaming response
123
- const response = await fetch("/api/mock/ai/default?firstChunkDelayMs=4800", {
123
+ const response = await fetch("/api/default?firstChunkDelayMs=4800", {
124
124
  method: "POST",
125
125
  headers: { "Content-Type": "application/json" },
126
126
  body: JSON.stringify({}),
127
127
  });
128
128
 
129
129
  // Use ?transport=json to get JSON format
130
- const jsonResponse = await fetch("/api/mock/ai/default?transport=json");
130
+ const jsonResponse = await fetch("/api/default?transport=json");
131
131
  ```
132
132
 
133
133
  **2. Plugin option `defaultScenario` (global)**
@@ -150,6 +150,23 @@ aiMockPlugin({
150
150
 
151
151
  When `defaultScenario` is not set, the `normal` scenario is used (no delay, completes normally).
152
152
 
153
+ **3. Plugin option `jsonApis` (specify JSON-returning APIs)**
154
+
155
+ Configure in `vite.config.ts` to specify which API paths should return JSON format instead of SSE:
156
+
157
+ ```ts
158
+ aiMockPlugin({
159
+ endpoint: "/api",
160
+ jsonApis: [
161
+ "/api/config", // exact match
162
+ "/api/history", // exact match
163
+ /^\/api\/static\/.*/, // regex match
164
+ ],
165
+ });
166
+ ```
167
+
168
+ Precedence: `?transport=json` / `?transport=sse` > `jsonApis` config > default SSE
169
+
153
170
  ## Mock file format
154
171
 
155
172
  Each file is a JSON object with a `chunks` array. Every chunk maps to one SSE event:
@@ -175,18 +192,18 @@ Each file is a JSON object with a `chunks` array. Every chunk maps to one SSE ev
175
192
 
176
193
  ### Real-world format examples
177
194
 
178
- 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:
195
+ The `data` field can mirror any real API response. The package ships with ready-to-use examples in `mock/` — copy them into your project as a starting point:
179
196
 
180
- | File | Provider |
181
- | -------------------------------- | ------------------- |
182
- | `mock/ai/openai.json` | OpenAI / compatible |
183
- | `mock/ai/claude.json` | Anthropic Claude |
184
- | `mock/ai/gemini.json` | Google Gemini |
185
- | `mock/ai/deepseek.json` | DeepSeek |
186
- | `mock/ai/deepseek-reasoner.json` | DeepSeek Reasoner |
187
- | `mock/ai/qwen.json` | Qwen (Alibaba) |
188
- | `mock/ai/qwen-thinking.json` | Qwen Thinking |
189
- | `mock/ai/doubao.json` | Doubao (ByteDance) |
197
+ | File | Provider |
198
+ | --------------------------- | ------------------- |
199
+ | `mock/openai.json` | OpenAI / compatible |
200
+ | `mock/claude.json` | Anthropic Claude |
201
+ | `mock/gemini.json` | Google Gemini |
202
+ | `mock/deepseek.json` | DeepSeek |
203
+ | `mock/deepseek-reasoner.json` | DeepSeek Reasoner |
204
+ | `mock/qwen.json` | Qwen (Alibaba) |
205
+ | `mock/qwen-thinking.json` | Qwen Thinking |
206
+ | `mock/doubao.json` | Doubao (ByteDance) |
190
207
 
191
208
  **OpenAI / compatible** (`openai.json`) — `data` ends with `"[DONE]"` string:
192
209
 
@@ -278,7 +295,7 @@ import { DefaultChatTransport } from "ai";
278
295
 
279
296
  const { messages, sendMessage, status } = useChat({
280
297
  transport: new DefaultChatTransport({
281
- api: "/api/mock/ai/chat",
298
+ api: "/api/chat",
282
299
  }),
283
300
  });
284
301
  ```
@@ -295,10 +312,10 @@ const { messages, sendMessage, status } = useChat({
295
312
 
296
313
  ```ts
297
314
  // string (default)
298
- endpoint: "/api/mock/ai";
299
- // /api/mock/ai → file = "default"
300
- // /api/mock/ai/chat → file = "chat"
301
- // /api/mock/ai/i18n/zh-CN → file = "i18n/zh-CN" (nested directory)
315
+ endpoint: "/api";
316
+ // /api → file = "default"
317
+ // /api/chat → file = "chat"
318
+ // /api/i18n/zh-CN → file = "i18n/zh-CN" (nested directory)
302
319
 
303
320
  // RegExp
304
321
  endpoint: /^\/api\/ai\/.*/;
@@ -308,11 +325,11 @@ endpoint: /^\/api\/ai\/.*/;
308
325
  endpoint: ["/api/chat", /^\/v2\/ai\/.*/];
309
326
  ```
310
327
 
311
- Nested directories are supported. For example, `/api/mock/ai/i18n/zh-CN` maps to `mock/ai/i18n/zh-CN.json`.
328
+ Nested directories are supported. For example, `/api/i18n/zh-CN` maps to `mock/i18n/zh-CN.json`.
312
329
 
313
- - `/api/mock/ai`
314
- - `/api/mock/ai/<file>`
315
- - `/api/mock/ai/<dir>/<file>` (nested)
330
+ - `/api`
331
+ - `/api/<file>`
332
+ - `/api/<dir>/<file>` (nested)
316
333
  - `?file=<file>` or `?file=<dir>/<file>`
317
334
 
318
335
  ## Test
@@ -333,4 +350,73 @@ pnpm build
333
350
  pnpm release:npm
334
351
  ```
335
352
 
336
- `prepublishOnly` will automatically run build, tests and typecheck..
353
+ `prepublishOnly` will automatically run build, tests and typecheck.
354
+
355
+ ## Configuration Options
356
+
357
+ ### Plugin Options `AiMockPluginOptions`
358
+
359
+ | Option | Type | Default | Description |
360
+ | ----------------- | ------------------------------------------ | ---------------- | --------------------------------------------------------------- |
361
+ | `dataDir` | `string` | `"mock"` | Directory for mock files, relative to project root |
362
+ | `endpoint` | `string \| RegExp \| (string \| RegExp)[]` | `"/api"` | API path to intercept, supports string, RegExp, or array |
363
+ | `defaultScenario` | `DefaultScenarioConfig` | `undefined` | Global default scenario config, can be overridden by URL params |
364
+ | `jsonApis` | `(string \| RegExp)[]` | `undefined` | List of API paths that should return JSON format |
365
+
366
+ ### Scenario Config `DefaultScenarioConfig`
367
+
368
+ | Option | Type | Default | Description |
369
+ | ------------------- | -------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
370
+ | `scenario` | `ScenarioName` | `undefined` | Preset scenario name: `normal`, `first-delay`, `jitter`, `disconnect`, `timeout`, `error`, `malformed`, `duplicate`, `out-of-order`, `reconnect`, `heartbeat` |
371
+ | `firstChunkDelayMs` | `number` | `0` | Delay before sending first chunk (ms) |
372
+ | `minIntervalMs` | `number` | `200` | Minimum interval between chunks (ms) |
373
+ | `maxIntervalMs` | `number` | `700` | Maximum interval between chunks (ms) |
374
+ | `disconnectAt` | `number` | `-1` | Disconnect at chunk N (-1 to disable) |
375
+ | `stallAfter` | `number` | `-1` | Stop sending after chunk N (-1 to disable) |
376
+ | `stallMs` | `number` | `30000` | Wait time after stalling (ms) |
377
+ | `errorAt` | `number` | `-1` | Send error event at chunk N (-1 to disable) |
378
+ | `errorMessage` | `string` | `"mock_error"` | Error event message content |
379
+ | `malformedAt` | `number` | `-1` | Send malformed data at chunk N (-1 to disable) |
380
+ | `duplicateAt` | `number` | `-1` | Send duplicate chunk at N (-1 to disable) |
381
+ | `outOfOrder` | `boolean` | `false` | Shuffle chunk order (swaps chunks 2 and 3) |
382
+ | `heartbeatMs` | `number` | `0` | Heartbeat interval (ms), 0 to disable |
383
+ | `reconnect` | `boolean` | `false` | Enable reconnect mode, use with `lastEventId` |
384
+
385
+ ### URL Parameters
386
+
387
+ In addition to scenario config params, the following URL-only params are supported:
388
+
389
+ | Param | Type | Default | Description |
390
+ | ----------------- | ------------------- | ----------- | ------------------------------------------------------------ |
391
+ | `file` | `string` | `"default"` | Mock file name (without `.json` extension) |
392
+ | `transport` | `"sse" \| "json"` | `"sse"` | Response format: `sse` for streaming, `json` for direct JSON |
393
+ | `httpErrorStatus` | `number` | `0` | Return specified HTTP error status (e.g., 401, 500) |
394
+ | `includeDone` | `"true" \| "false"` | `"true"` | Whether to send `done` event at stream end |
395
+ | `lastEventId` | `string` | `undefined` | Last event ID for reconnection, resumes after this ID |
396
+
397
+ ### Full Configuration Example
398
+
399
+ ```ts
400
+ import { defineConfig } from "vite";
401
+ import { aiMockPlugin } from "vite-plugin-ai-mock";
402
+
403
+ export default defineConfig({
404
+ plugins: [
405
+ aiMockPlugin({
406
+ // Mock file directory
407
+ dataDir: "mock",
408
+ // API path to intercept
409
+ endpoint: "/api",
410
+ // Global default scenario
411
+ defaultScenario: {
412
+ scenario: "jitter",
413
+ firstChunkDelayMs: 500,
414
+ minIntervalMs: 100,
415
+ maxIntervalMs: 800,
416
+ },
417
+ // APIs that return JSON format
418
+ jsonApis: ["/api/config", /^\/api\/static\/.*/],
419
+ }),
420
+ ],
421
+ });
422
+ ```
package/README.zh-CN.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  > [English](./README.md) | 中文
6
6
 
7
- - 从 `mock/ai/*.json` 读取 mock 文件
7
+ - 从 `mock/*.json` 读取 mock 文件
8
8
  - 默认返回 SSE 流式响应
9
9
  - 使用 `?transport=json` 获取 JSON 格式响应
10
10
  - 支持 11 种流式场景,通过请求参数控制
@@ -31,15 +31,15 @@ project/
31
31
  │ └── default.json
32
32
  ├── src/
33
33
  └── vite.config.ts
34
+ ```
34
35
 
36
+ **请求示例**
35
37
 
36
-
37
-
38
-
39
-
40
-
41
-
42
-
38
+ ```ts
39
+ // SSE 流式响应(默认)
40
+ const res = await fetch("/api/chat");
41
+ // JSON 响应
42
+ const json = await fetch("/api/chat?transport=json");
43
43
  ```
44
44
 
45
45
  </td>
@@ -54,14 +54,14 @@ import { aiMockPlugin } from "vite-plugin-ai-mock";
54
54
  export default defineConfig({
55
55
  plugins: [
56
56
  aiMockPlugin({
57
- dataDir: "mock/ai",
58
- endpoint: "/api/mock/ai", // /api/mock/ai/chat → chat.json
57
+ dataDir: "mock",
58
+ endpoint: "/api", // /api/chat → chat.json
59
59
  }),
60
60
  ],
61
61
  });
62
62
  ```
63
63
 
64
- **mock/ai/chat.json**
64
+ **mock/chat.json**
65
65
 
66
66
  ```json
67
67
  {
@@ -108,20 +108,20 @@ export default defineConfig({
108
108
  直接在请求 URL 上附加参数,适合临时调试单个接口:
109
109
 
110
110
  ```
111
- /api/mock/ai/default?scenario=jitter
112
- /api/mock/ai/default?firstChunkDelayMs=1000&errorAt=3
111
+ /api/default?scenario=jitter
112
+ /api/default?firstChunkDelayMs=1000&errorAt=3
113
113
  ```
114
114
 
115
115
  ```ts
116
116
  // 默认返回 SSE 流式响应
117
- const response = await fetch("/api/mock/ai/default?firstChunkDelayMs=4800", {
117
+ const response = await fetch("/api/default?firstChunkDelayMs=4800", {
118
118
  method: "POST",
119
119
  headers: { "Content-Type": "application/json" },
120
120
  body: JSON.stringify({}),
121
121
  });
122
122
 
123
123
  // 使用 ?transport=json 获取 JSON 格式
124
- const jsonResponse = await fetch("/api/mock/ai/default?transport=json");
124
+ const jsonResponse = await fetch("/api/default?transport=json");
125
125
  ```
126
126
 
127
127
  **2. 插件选项 `defaultScenario`(全局生效)**
@@ -144,6 +144,23 @@ aiMockPlugin({
144
144
 
145
145
  不配置 `defaultScenario` 时,默认使用 `normal` 场景(无延迟,正常完成)。
146
146
 
147
+ **3. 插件选项 `jsonApis`(指定返回 JSON 格式的 API)**
148
+
149
+ 在 `vite.config.ts` 中配置,指定哪些 API 路径返回 JSON 格式而不是 SSE 格式:
150
+
151
+ ```ts
152
+ aiMockPlugin({
153
+ endpoint: "/api",
154
+ jsonApis: [
155
+ "/api/config", // 精确匹配
156
+ "/api/history", // 精确匹配
157
+ /^\/api\/static\/.*/, // 正则匹配
158
+ ],
159
+ });
160
+ ```
161
+
162
+ 优先级:`?transport=json` / `?transport=sse` > `jsonApis` 配置 > 默认 SSE
163
+
147
164
  ## Mock 文件格式
148
165
 
149
166
  每个文件是一个包含 `chunks` 数组的 JSON 对象,每个 chunk 对应一条 SSE 事件:
@@ -169,18 +186,18 @@ aiMockPlugin({
169
186
 
170
187
  ### 主流格式示例
171
188
 
172
- `data` 字段可以完整模拟真实 API 的响应结构。npm 包内置了以下示例文件(位于 `mock/ai/`),可直接复制到项目中使用:
189
+ `data` 字段可以完整模拟真实 API 的响应结构。npm 包内置了以下示例文件(位于 `mock/`),可直接复制到项目中使用:
173
190
 
174
- | 文件 | 提供商 |
175
- | -------------------------------- | ----------------- |
176
- | `mock/ai/openai.json` | OpenAI / 兼容格式 |
177
- | `mock/ai/claude.json` | Anthropic Claude |
178
- | `mock/ai/gemini.json` | Google Gemini |
179
- | `mock/ai/deepseek.json` | DeepSeek |
180
- | `mock/ai/deepseek-reasoner.json` | DeepSeek Reasoner |
181
- | `mock/ai/qwen.json` | 通义千问(阿里) |
182
- | `mock/ai/qwen-thinking.json` | 通义千问 Thinking |
183
- | `mock/ai/doubao.json` | 豆包(字节跳动) |
191
+ | 文件 | 提供商 |
192
+ | --------------------------- | ----------------- |
193
+ | `mock/openai.json` | OpenAI / 兼容格式 |
194
+ | `mock/claude.json` | Anthropic Claude |
195
+ | `mock/gemini.json` | Google Gemini |
196
+ | `mock/deepseek.json` | DeepSeek |
197
+ | `mock/deepseek-reasoner.json` | DeepSeek Reasoner |
198
+ | `mock/qwen.json` | 通义千问(阿里) |
199
+ | `mock/qwen-thinking.json` | 通义千问 Thinking |
200
+ | `mock/doubao.json` | 豆包(字节跳动) |
184
201
 
185
202
  **OpenAI / 兼容格式**(`openai.json`)——最后一条 `data` 为字符串 `"[DONE]"`:
186
203
 
@@ -272,7 +289,7 @@ import { DefaultChatTransport } from "ai";
272
289
 
273
290
  const { messages, sendMessage, status } = useChat({
274
291
  transport: new DefaultChatTransport({
275
- api: "/api/mock/ai/chat",
292
+ api: "/api/chat",
276
293
  }),
277
294
  });
278
295
  ```
@@ -289,10 +306,10 @@ const { messages, sendMessage, status } = useChat({
289
306
 
290
307
  ```ts
291
308
  // string(默认)
292
- endpoint: "/api/mock/ai";
293
- // /api/mock/ai → file = "default"
294
- // /api/mock/ai/chat → file = "chat"
295
- // /api/mock/ai/i18n/zh-CN → file = "i18n/zh-CN"(多层级目录)
309
+ endpoint: "/api";
310
+ // /api → file = "default"
311
+ // /api/chat → file = "chat"
312
+ // /api/i18n/zh-CN → file = "i18n/zh-CN"(多层级目录)
296
313
 
297
314
  // RegExp
298
315
  endpoint: /^\/api\/ai\/.*/;
@@ -302,11 +319,11 @@ endpoint: /^\/api\/ai\/.*/;
302
319
  endpoint: ["/api/chat", /^\/v2\/ai\/.*/];
303
320
  ```
304
321
 
305
- 支持多层级目录。例如 `/api/mock/ai/i18n/zh-CN` 会映射到 `mock/ai/i18n/zh-CN.json`。
322
+ 支持多层级目录。例如 `/api/i18n/zh-CN` 会映射到 `mock/i18n/zh-CN.json`。
306
323
 
307
- - `/api/mock/ai`
308
- - `/api/mock/ai/<file>`
309
- - `/api/mock/ai/<dir>/<file>`(多层级)
324
+ - `/api`
325
+ - `/api/<file>`
326
+ - `/api/<dir>/<file>`(多层级)
310
327
  - `?file=<file>` 或 `?file=<dir>/<file>`
311
328
 
312
329
  ## 测试
@@ -328,3 +345,75 @@ pnpm release:npm
328
345
  ```
329
346
 
330
347
  `prepublishOnly` 会自动执行构建、测试和类型检查。
348
+
349
+ ## 配置选项
350
+
351
+ ### 插件配置 `AiMockPluginOptions`
352
+
353
+ | 选项 | 类型 | 默认值 | 说明 |
354
+ | --- | --- | --- | --- |
355
+ | `dataDir` | `string` | `"mock"` | mock 文件所在目录,相对于项目根目录 |
356
+ | `endpoint` | `string \| RegExp \| (string \| RegExp)[]` | `"/api"` | 拦截的 API 路径,支持字符串、正则或数组 |
357
+ | `defaultScenario` | `DefaultScenarioConfig` | `undefined` | 全局默认场景配置,可被 URL 参数覆盖 |
358
+ | `jsonApis` | `(string \| RegExp)[]` | `undefined` | 指定返回 JSON 格式的 API 路径列表 |
359
+
360
+ ### 场景配置 `DefaultScenarioConfig`
361
+
362
+ | 选项 | 类型 | 默认值 | 说明 |
363
+ | --- | --- | --- | --- |
364
+ | `scenario` | `ScenarioName` | `undefined` | 预设场景名:`normal`、`first-delay`、`jitter`、`disconnect`、`timeout`、`error`、`malformed`、`duplicate`、`out-of-order`、`reconnect`、`heartbeat` |
365
+ | `firstChunkDelayMs` | `number` | `0` | 首个数据块发送前的延迟时间(毫秒) |
366
+ | `minIntervalMs` | `number` | `200` | 数据块之间的最小间隔时间(毫秒) |
367
+ | `maxIntervalMs` | `number` | `700` | 数据块之间的最大间隔时间(毫秒) |
368
+ | `disconnectAt` | `number` | `-1` | 在第 N 个数据块处断开连接(-1 为不断开) |
369
+ | `stallAfter` | `number` | `-1` | 在第 N 个数据块后停止发送(-1 为不停止) |
370
+ | `stallMs` | `number` | `30000` | 停止发送后等待的时间(毫秒) |
371
+ | `errorAt` | `number` | `-1` | 在第 N 个数据块处发送错误事件(-1 为不发送) |
372
+ | `errorMessage` | `string` | `"mock_error"` | 错误事件的消息内容 |
373
+ | `malformedAt` | `number` | `-1` | 在第 N 个数据块处发送格式错误的数据(-1 为不发送) |
374
+ | `duplicateAt` | `number` | `-1` | 在第 N 个数据块处发送重复数据(-1 为不发送) |
375
+ | `outOfOrder` | `boolean` | `false` | 是否打乱数据块顺序(交换第 2、3 块) |
376
+ | `heartbeatMs` | `number` | `0` | 心跳间隔时间(毫秒),0 为不发送心跳 |
377
+ | `reconnect` | `boolean` | `false` | 是否启用重连模式,配合 `lastEventId` 使用 |
378
+
379
+ ### URL 参数
380
+
381
+ 除了上述场景配置参数外,还支持以下 URL 专用参数:
382
+
383
+ | 参数 | 类型 | 默认值 | 说明 |
384
+ | --- | --- | --- | --- |
385
+ | `file` | `string` | `"default"` | 指定 mock 文件名(不含 `.json` 后缀) |
386
+ | `transport` | `"sse" \| "json"` | `"sse"` | 响应格式:`sse` 流式响应,`json` 直接返回 JSON |
387
+ | `httpErrorStatus` | `number` | `0` | 返回指定 HTTP 状态码错误(如 401、500) |
388
+ | `includeDone` | `"true" \| "false"` | `"true"` | 是否在流结束时发送 `done` 事件 |
389
+ | `lastEventId` | `string` | `undefined` | 断线重连时的最后事件 ID,从该 ID 之后继续发送 |
390
+
391
+ ### 完整配置示例
392
+
393
+ ```ts
394
+ import { defineConfig } from "vite";
395
+ import { aiMockPlugin } from "vite-plugin-ai-mock";
396
+
397
+ export default defineConfig({
398
+ plugins: [
399
+ aiMockPlugin({
400
+ // mock 文件目录
401
+ dataDir: "mock",
402
+ // 拦截的 API 路径
403
+ endpoint: "/api",
404
+ // 全局默认场景
405
+ defaultScenario: {
406
+ scenario: "jitter",
407
+ firstChunkDelayMs: 500,
408
+ minIntervalMs: 100,
409
+ maxIntervalMs: 800,
410
+ },
411
+ // 返回 JSON 格式的 API 列表
412
+ jsonApis: [
413
+ "/api/config",
414
+ /^\/api\/static\/.*/,
415
+ ],
416
+ }),
417
+ ],
418
+ });
419
+ ```
package/dist/index.cjs CHANGED
@@ -161,9 +161,21 @@ function applyChunkMutations(chunks, options) {
161
161
  }
162
162
  return result;
163
163
  }
164
- function isSseRequest(reqUrl) {
164
+ function isJsonApi(pathname, jsonApis) {
165
+ if (!jsonApis || jsonApis.length === 0) return false;
166
+ return jsonApis.some((pattern) => {
167
+ if (typeof pattern === "string") {
168
+ return pathname === pattern || pathname.startsWith(`${pattern}/`);
169
+ }
170
+ return pattern.test(pathname);
171
+ });
172
+ }
173
+ function isSseRequest(reqUrl, jsonApis) {
165
174
  const transport = reqUrl.searchParams.get("transport");
166
- return transport !== "json";
175
+ if (transport === "json") return false;
176
+ if (transport === "sse") return true;
177
+ if (isJsonApi(reqUrl.pathname, jsonApis)) return false;
178
+ return true;
167
179
  }
168
180
  function writeSseEvent(res, options) {
169
181
  if (options.id) res.write(`id: ${options.id}
@@ -199,6 +211,7 @@ function aiMockPlugin(config) {
199
211
  const dataDir = config?.dataDir ?? "mock/ai";
200
212
  const endpoint = config?.endpoint ?? AI_MOCK_BASE;
201
213
  const defaultScenario = config?.defaultScenario;
214
+ const jsonApis = config?.jsonApis;
202
215
  return {
203
216
  name: "vite-plugin-ai-mock",
204
217
  configureServer(server) {
@@ -243,7 +256,7 @@ function aiMockPlugin(config) {
243
256
  }
244
257
  const raw = readJsonFile(filePath);
245
258
  const chunks = applyChunkMutations(normalizeChunks(raw), options);
246
- if (!isSseRequest(reqUrl)) {
259
+ if (!isSseRequest(reqUrl, jsonApis)) {
247
260
  console.log("[aiMockPlugin] Handling as JSON response");
248
261
  res.statusCode = 200;
249
262
  res.setHeader("Content-Type", "application/json; charset=utf-8");
@@ -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<\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 // Allow forward slash for subdirectories, but prevent path traversal\n return name\n .replace(/\\.\\./g, \"\") // Remove path traversal attempts\n .replace(/[^a-zA-Z0-9._\\-/]/g, \"\") // Keep / for subdirectories\n .replace(/\\/+/g, \"/\") // Collapse multiple slashes\n .replace(/^\\/|\\/$/g, \"\"); // Trim leading/trailing slashes\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(reqUrl: URL): boolean {\n const transport = reqUrl.searchParams.get(\"transport\");\n // Default to SSE, only use JSON when explicitly requested via transport=json\n return transport !== \"json\";\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(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(JSON.stringify(raw));\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"]}
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 * List of API paths that should return JSON format instead of SSE.\n * Supports string paths and RegExp patterns.\n * @example ['/api/chat/history', new RegExp('/api/config/.*')]\n */\n jsonApis?: (string | RegExp)[];\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 // Allow forward slash for subdirectories, but prevent path traversal\n return name\n .replace(/\\.\\./g, \"\") // Remove path traversal attempts\n .replace(/[^a-zA-Z0-9._\\-/]/g, \"\") // Keep / for subdirectories\n .replace(/\\/+/g, \"/\") // Collapse multiple slashes\n .replace(/^\\/|\\/$/g, \"\"); // Trim leading/trailing slashes\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 isJsonApi(pathname: string, jsonApis?: (string | RegExp)[]): boolean {\n if (!jsonApis || jsonApis.length === 0) return false;\n return jsonApis.some((pattern) => {\n if (typeof pattern === \"string\") {\n return pathname === pattern || pathname.startsWith(`${pattern}/`);\n }\n return pattern.test(pathname);\n });\n}\n\nfunction isSseRequest(reqUrl: URL, jsonApis?: (string | RegExp)[]): boolean {\n const transport = reqUrl.searchParams.get(\"transport\");\n // If transport is explicitly set, use that\n if (transport === \"json\") return false;\n if (transport === \"sse\") return true;\n // Check if the API is in the jsonApis list\n if (isJsonApi(reqUrl.pathname, jsonApis)) return false;\n // Default to SSE\n return true;\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 const jsonApis = config?.jsonApis;\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(reqUrl, jsonApis)) {\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(JSON.stringify(raw));\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
@@ -29,6 +29,12 @@ interface AiMockPluginOptions {
29
29
  * @default undefined (uses 'normal' scenario with no preset)
30
30
  */
31
31
  defaultScenario?: DefaultScenarioConfig;
32
+ /**
33
+ * List of API paths that should return JSON format instead of SSE.
34
+ * Supports string paths and RegExp patterns.
35
+ * @example ['/api/chat/history', new RegExp('/api/config/.*')]
36
+ */
37
+ jsonApis?: (string | RegExp)[];
32
38
  }
33
39
  declare const SCENARIO_PRESETS: {
34
40
  readonly normal: {};
package/dist/index.d.ts CHANGED
@@ -29,6 +29,12 @@ interface AiMockPluginOptions {
29
29
  * @default undefined (uses 'normal' scenario with no preset)
30
30
  */
31
31
  defaultScenario?: DefaultScenarioConfig;
32
+ /**
33
+ * List of API paths that should return JSON format instead of SSE.
34
+ * Supports string paths and RegExp patterns.
35
+ * @example ['/api/chat/history', new RegExp('/api/config/.*')]
36
+ */
37
+ jsonApis?: (string | RegExp)[];
32
38
  }
33
39
  declare const SCENARIO_PRESETS: {
34
40
  readonly normal: {};
package/dist/index.js CHANGED
@@ -127,9 +127,21 @@ function applyChunkMutations(chunks, options) {
127
127
  }
128
128
  return result;
129
129
  }
130
- function isSseRequest(reqUrl) {
130
+ function isJsonApi(pathname, jsonApis) {
131
+ if (!jsonApis || jsonApis.length === 0) return false;
132
+ return jsonApis.some((pattern) => {
133
+ if (typeof pattern === "string") {
134
+ return pathname === pattern || pathname.startsWith(`${pattern}/`);
135
+ }
136
+ return pattern.test(pathname);
137
+ });
138
+ }
139
+ function isSseRequest(reqUrl, jsonApis) {
131
140
  const transport = reqUrl.searchParams.get("transport");
132
- return transport !== "json";
141
+ if (transport === "json") return false;
142
+ if (transport === "sse") return true;
143
+ if (isJsonApi(reqUrl.pathname, jsonApis)) return false;
144
+ return true;
133
145
  }
134
146
  function writeSseEvent(res, options) {
135
147
  if (options.id) res.write(`id: ${options.id}
@@ -165,6 +177,7 @@ function aiMockPlugin(config) {
165
177
  const dataDir = config?.dataDir ?? "mock/ai";
166
178
  const endpoint = config?.endpoint ?? AI_MOCK_BASE;
167
179
  const defaultScenario = config?.defaultScenario;
180
+ const jsonApis = config?.jsonApis;
168
181
  return {
169
182
  name: "vite-plugin-ai-mock",
170
183
  configureServer(server) {
@@ -209,7 +222,7 @@ function aiMockPlugin(config) {
209
222
  }
210
223
  const raw = readJsonFile(filePath);
211
224
  const chunks = applyChunkMutations(normalizeChunks(raw), options);
212
- if (!isSseRequest(reqUrl)) {
225
+ if (!isSseRequest(reqUrl, jsonApis)) {
213
226
  console.log("[aiMockPlugin] Handling as JSON response");
214
227
  res.statusCode = 200;
215
228
  res.setHeader("Content-Type", "application/json; charset=utf-8");
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<\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 // Allow forward slash for subdirectories, but prevent path traversal\n return name\n .replace(/\\.\\./g, \"\") // Remove path traversal attempts\n .replace(/[^a-zA-Z0-9._\\-/]/g, \"\") // Keep / for subdirectories\n .replace(/\\/+/g, \"/\") // Collapse multiple slashes\n .replace(/^\\/|\\/$/g, \"\"); // Trim leading/trailing slashes\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(reqUrl: URL): boolean {\n const transport = reqUrl.searchParams.get(\"transport\");\n // Default to SSE, only use JSON when explicitly requested via transport=json\n return transport !== \"json\";\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(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(JSON.stringify(raw));\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":[]}
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 * List of API paths that should return JSON format instead of SSE.\n * Supports string paths and RegExp patterns.\n * @example ['/api/chat/history', new RegExp('/api/config/.*')]\n */\n jsonApis?: (string | RegExp)[];\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 // Allow forward slash for subdirectories, but prevent path traversal\n return name\n .replace(/\\.\\./g, \"\") // Remove path traversal attempts\n .replace(/[^a-zA-Z0-9._\\-/]/g, \"\") // Keep / for subdirectories\n .replace(/\\/+/g, \"/\") // Collapse multiple slashes\n .replace(/^\\/|\\/$/g, \"\"); // Trim leading/trailing slashes\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 isJsonApi(pathname: string, jsonApis?: (string | RegExp)[]): boolean {\n if (!jsonApis || jsonApis.length === 0) return false;\n return jsonApis.some((pattern) => {\n if (typeof pattern === \"string\") {\n return pathname === pattern || pathname.startsWith(`${pattern}/`);\n }\n return pattern.test(pathname);\n });\n}\n\nfunction isSseRequest(reqUrl: URL, jsonApis?: (string | RegExp)[]): boolean {\n const transport = reqUrl.searchParams.get(\"transport\");\n // If transport is explicitly set, use that\n if (transport === \"json\") return false;\n if (transport === \"sse\") return true;\n // Check if the API is in the jsonApis list\n if (isJsonApi(reqUrl.pathname, jsonApis)) return false;\n // Default to SSE\n return true;\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 const jsonApis = config?.jsonApis;\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(reqUrl, jsonApis)) {\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(JSON.stringify(raw));\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":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vite-plugin-ai-mock",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "A Vite plugin for AI streaming mock (SSE/JSON) with configurable scenarios.",
5
5
  "license": "MIT",
6
6
  "type": "module",