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