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 +122 -36
- package/README.zh-CN.md +124 -35
- package/dist/index.cjs +16 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +16 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
|
64
|
-
endpoint: "/api
|
|
63
|
+
dataDir: "mock",
|
|
64
|
+
endpoint: "/api", // /api/chat → chat.json
|
|
65
65
|
}),
|
|
66
66
|
],
|
|
67
67
|
});
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
-
**mock/
|
|
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/
|
|
118
|
-
/api/
|
|
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/
|
|
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/
|
|
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
|
|
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
|
|
181
|
-
|
|
|
182
|
-
| `mock/
|
|
183
|
-
| `mock/
|
|
184
|
-
| `mock/
|
|
185
|
-
| `mock/
|
|
186
|
-
| `mock/
|
|
187
|
-
| `mock/
|
|
188
|
-
| `mock/
|
|
189
|
-
| `mock/
|
|
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/
|
|
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
|
|
299
|
-
// /api
|
|
300
|
-
// /api/
|
|
301
|
-
// /api/
|
|
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/
|
|
328
|
+
Nested directories are supported. For example, `/api/i18n/zh-CN` maps to `mock/i18n/zh-CN.json`.
|
|
312
329
|
|
|
313
|
-
- `/api
|
|
314
|
-
- `/api
|
|
315
|
-
- `/api
|
|
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
|
|
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
|
|
58
|
-
endpoint: "/api
|
|
57
|
+
dataDir: "mock",
|
|
58
|
+
endpoint: "/api", // /api/chat → chat.json
|
|
59
59
|
}),
|
|
60
60
|
],
|
|
61
61
|
});
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
-
**mock/
|
|
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/
|
|
112
|
-
/api/
|
|
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/
|
|
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/
|
|
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
|
|
189
|
+
`data` 字段可以完整模拟真实 API 的响应结构。npm 包内置了以下示例文件(位于 `mock/`),可直接复制到项目中使用:
|
|
173
190
|
|
|
174
|
-
| 文件
|
|
175
|
-
|
|
|
176
|
-
| `mock/
|
|
177
|
-
| `mock/
|
|
178
|
-
| `mock/
|
|
179
|
-
| `mock/
|
|
180
|
-
| `mock/
|
|
181
|
-
| `mock/
|
|
182
|
-
| `mock/
|
|
183
|
-
| `mock/
|
|
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/
|
|
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
|
|
293
|
-
// /api
|
|
294
|
-
// /api/
|
|
295
|
-
// /api/
|
|
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/
|
|
322
|
+
支持多层级目录。例如 `/api/i18n/zh-CN` 会映射到 `mock/i18n/zh-CN.json`。
|
|
306
323
|
|
|
307
|
-
- `/api
|
|
308
|
-
- `/api
|
|
309
|
-
- `/api
|
|
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
|
|
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
|
-
|
|
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");
|
package/dist/index.cjs.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":["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
|
|
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
|
-
|
|
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":[]}
|