@websublime/vite-plugin-open-api-server 0.19.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/LICENSE +21 -0
- package/README.md +207 -0
- package/dist/index.d.ts +433 -0
- package/dist/index.js +585 -0
- package/dist/index.js.map +1 -0
- package/package.json +57 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
import { createOpenApiServer, executeSeeds } from '@websublime/vite-plugin-open-api-core';
|
|
2
|
+
export { defineHandlers, defineSeeds } from '@websublime/vite-plugin-open-api-core';
|
|
3
|
+
import pc from 'picocolors';
|
|
4
|
+
import path2 from 'path';
|
|
5
|
+
import fg from 'fast-glob';
|
|
6
|
+
|
|
7
|
+
// src/plugin.ts
|
|
8
|
+
function printBanner(info, options) {
|
|
9
|
+
if (options.silent) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const logger = options.logger ?? console;
|
|
13
|
+
const log = (msg) => logger.info(msg);
|
|
14
|
+
const BOX = {
|
|
15
|
+
topLeft: "\u256D",
|
|
16
|
+
topRight: "\u256E",
|
|
17
|
+
bottomLeft: "\u2570",
|
|
18
|
+
bottomRight: "\u256F",
|
|
19
|
+
horizontal: "\u2500",
|
|
20
|
+
vertical: "\u2502"
|
|
21
|
+
};
|
|
22
|
+
const width = 56;
|
|
23
|
+
const horizontalLine = BOX.horizontal.repeat(width - 2);
|
|
24
|
+
log("");
|
|
25
|
+
log(pc.cyan(`${BOX.topLeft}${horizontalLine}${BOX.topRight}`));
|
|
26
|
+
log(
|
|
27
|
+
pc.cyan(BOX.vertical) + centerText("\u{1F680} OpenAPI Mock Server", width - 2) + pc.cyan(BOX.vertical)
|
|
28
|
+
);
|
|
29
|
+
log(pc.cyan(`${BOX.bottomLeft}${horizontalLine}${BOX.bottomRight}`));
|
|
30
|
+
log("");
|
|
31
|
+
log(
|
|
32
|
+
` ${pc.bold(pc.white("API:"))} ${pc.green(info.title)} ${pc.dim(`v${info.version}`)}`
|
|
33
|
+
);
|
|
34
|
+
log(` ${pc.bold(pc.white("Server:"))} ${pc.cyan(`http://localhost:${info.port}`)}`);
|
|
35
|
+
log(
|
|
36
|
+
` ${pc.bold(pc.white("Proxy:"))} ${pc.yellow(info.proxyPath)} ${pc.dim("\u2192")} ${pc.dim(`localhost:${info.port}`)}`
|
|
37
|
+
);
|
|
38
|
+
log("");
|
|
39
|
+
const stats = [
|
|
40
|
+
{ label: "Endpoints", value: info.endpointCount, color: pc.blue },
|
|
41
|
+
{ label: "Handlers", value: info.handlerCount, color: pc.green },
|
|
42
|
+
{ label: "Seeds", value: info.seedCount, color: pc.magenta }
|
|
43
|
+
];
|
|
44
|
+
const statsLine = stats.map((s) => `${pc.dim(`${s.label}:`)} ${s.color(String(s.value))}`).join(pc.dim(" \u2502 "));
|
|
45
|
+
log(` ${statsLine}`);
|
|
46
|
+
log("");
|
|
47
|
+
if (info.devtools) {
|
|
48
|
+
log(
|
|
49
|
+
` ${pc.bold(pc.white("DevTools:"))} ${pc.cyan(`http://localhost:${info.port}/_devtools`)}`
|
|
50
|
+
);
|
|
51
|
+
log(` ${pc.bold(pc.white("API Info:"))} ${pc.cyan(`http://localhost:${info.port}/_api`)}`);
|
|
52
|
+
log("");
|
|
53
|
+
}
|
|
54
|
+
log(pc.dim(" Press Ctrl+C to stop the server"));
|
|
55
|
+
log("");
|
|
56
|
+
}
|
|
57
|
+
var ANSI_ESCAPE_REGEX = /\x1b\[[0-9;]*m/g;
|
|
58
|
+
function centerText(text, width) {
|
|
59
|
+
const visibleLength = text.replace(ANSI_ESCAPE_REGEX, "").length;
|
|
60
|
+
const padding = Math.max(0, width - visibleLength);
|
|
61
|
+
const leftPad = Math.floor(padding / 2);
|
|
62
|
+
const rightPad = padding - leftPad;
|
|
63
|
+
return " ".repeat(leftPad) + text + " ".repeat(rightPad);
|
|
64
|
+
}
|
|
65
|
+
function extractBannerInfo(registry, document, handlerCount, seedCount, options) {
|
|
66
|
+
return {
|
|
67
|
+
port: options.port,
|
|
68
|
+
proxyPath: options.proxyPath,
|
|
69
|
+
title: document.info.title,
|
|
70
|
+
version: document.info.version,
|
|
71
|
+
endpointCount: registry.endpoints.size,
|
|
72
|
+
handlerCount,
|
|
73
|
+
seedCount,
|
|
74
|
+
devtools: options.devtools
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function printReloadNotification(type, count, options) {
|
|
78
|
+
if (options.silent) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const logger = options.logger ?? console;
|
|
82
|
+
const icon = type === "handlers" ? "\u{1F504}" : "\u{1F331}";
|
|
83
|
+
const label = type === "handlers" ? "Handlers" : "Seeds";
|
|
84
|
+
const color = type === "handlers" ? pc.green : pc.magenta;
|
|
85
|
+
logger.info(` ${icon} ${color(label)} reloaded: ${pc.bold(String(count))} ${type}`);
|
|
86
|
+
}
|
|
87
|
+
function printError(message, error, options) {
|
|
88
|
+
const logger = options.logger ?? console;
|
|
89
|
+
logger.error(`${pc.red("\u2716")} ${pc.bold(pc.red("Error:"))} ${message}`);
|
|
90
|
+
if (error instanceof Error) {
|
|
91
|
+
logger.error(pc.dim(` ${error.message}`));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// src/utils.ts
|
|
96
|
+
async function directoryExists(dirPath) {
|
|
97
|
+
try {
|
|
98
|
+
const fs = await import('fs/promises');
|
|
99
|
+
const stats = await fs.stat(dirPath);
|
|
100
|
+
return stats.isDirectory();
|
|
101
|
+
} catch {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// src/handlers.ts
|
|
107
|
+
async function loadHandlers(handlersDir, viteServer, cwd = process.cwd(), logger = console) {
|
|
108
|
+
const handlers = /* @__PURE__ */ new Map();
|
|
109
|
+
const absoluteDir = path2.resolve(cwd, handlersDir);
|
|
110
|
+
const dirExists = await directoryExists(absoluteDir);
|
|
111
|
+
if (!dirExists) {
|
|
112
|
+
return {
|
|
113
|
+
handlers,
|
|
114
|
+
fileCount: 0,
|
|
115
|
+
files: []
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
const pattern = "**/*.handlers.{ts,js,mjs}";
|
|
119
|
+
const files = await fg(pattern, {
|
|
120
|
+
cwd: absoluteDir,
|
|
121
|
+
absolute: false,
|
|
122
|
+
onlyFiles: true,
|
|
123
|
+
ignore: ["node_modules/**", "dist/**"]
|
|
124
|
+
});
|
|
125
|
+
for (const file of files) {
|
|
126
|
+
const absolutePath = path2.join(absoluteDir, file);
|
|
127
|
+
const fileHandlers = await loadHandlerFile(absolutePath, viteServer, logger);
|
|
128
|
+
for (const [operationId, handler] of Object.entries(fileHandlers)) {
|
|
129
|
+
if (handlers.has(operationId)) {
|
|
130
|
+
logger.warn(
|
|
131
|
+
`[vite-plugin-open-api-server] Duplicate handler for operationId "${operationId}" in ${file}. Using last definition.`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
handlers.set(operationId, handler);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
handlers,
|
|
139
|
+
fileCount: files.length,
|
|
140
|
+
files
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
async function loadHandlerFile(filePath, viteServer, logger) {
|
|
144
|
+
try {
|
|
145
|
+
const moduleNode = viteServer.moduleGraph.getModuleById(filePath);
|
|
146
|
+
if (moduleNode) {
|
|
147
|
+
viteServer.moduleGraph.invalidateModule(moduleNode);
|
|
148
|
+
}
|
|
149
|
+
const module = await viteServer.ssrLoadModule(filePath);
|
|
150
|
+
const handlers = module.default ?? module.handlers ?? module;
|
|
151
|
+
if (!handlers || typeof handlers !== "object") {
|
|
152
|
+
logger.warn(
|
|
153
|
+
`[vite-plugin-open-api-server] Invalid handler file ${filePath}: expected object export`
|
|
154
|
+
);
|
|
155
|
+
return {};
|
|
156
|
+
}
|
|
157
|
+
const validHandlers = {};
|
|
158
|
+
for (const [key, value] of Object.entries(handlers)) {
|
|
159
|
+
if (typeof value === "function") {
|
|
160
|
+
validHandlers[key] = value;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return validHandlers;
|
|
164
|
+
} catch (error) {
|
|
165
|
+
logger.error(
|
|
166
|
+
`[vite-plugin-open-api-server] Failed to load handler file ${filePath}:`,
|
|
167
|
+
error instanceof Error ? error.message : error
|
|
168
|
+
);
|
|
169
|
+
return {};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
async function getHandlerFiles(handlersDir, cwd = process.cwd()) {
|
|
173
|
+
const absoluteDir = path2.resolve(cwd, handlersDir);
|
|
174
|
+
const dirExists = await directoryExists(absoluteDir);
|
|
175
|
+
if (!dirExists) {
|
|
176
|
+
return [];
|
|
177
|
+
}
|
|
178
|
+
const pattern = "**/*.handlers.{ts,js,mjs}";
|
|
179
|
+
const files = await fg(pattern, {
|
|
180
|
+
cwd: absoluteDir,
|
|
181
|
+
absolute: true,
|
|
182
|
+
onlyFiles: true,
|
|
183
|
+
ignore: ["node_modules/**", "dist/**"]
|
|
184
|
+
});
|
|
185
|
+
return files;
|
|
186
|
+
}
|
|
187
|
+
async function createFileWatcher(options) {
|
|
188
|
+
const {
|
|
189
|
+
handlersDir,
|
|
190
|
+
seedsDir,
|
|
191
|
+
onHandlerChange,
|
|
192
|
+
onSeedChange,
|
|
193
|
+
cwd = process.cwd(),
|
|
194
|
+
logger = console
|
|
195
|
+
} = options;
|
|
196
|
+
const { watch } = await import('chokidar');
|
|
197
|
+
const watchers = [];
|
|
198
|
+
const readyPromises = [];
|
|
199
|
+
let isWatching = true;
|
|
200
|
+
const handlerPattern = "**/*.handlers.{ts,js,mjs}";
|
|
201
|
+
const seedPattern = "**/*.seeds.{ts,js,mjs}";
|
|
202
|
+
const safeInvoke = (callback, filePath, context) => {
|
|
203
|
+
Promise.resolve().then(() => callback(filePath)).catch((error) => {
|
|
204
|
+
logger.error(
|
|
205
|
+
`[vite-plugin-open-api-server] ${context} callback error for ${filePath}:`,
|
|
206
|
+
error
|
|
207
|
+
);
|
|
208
|
+
});
|
|
209
|
+
};
|
|
210
|
+
if (handlersDir && onHandlerChange) {
|
|
211
|
+
const absoluteHandlersDir = path2.resolve(cwd, handlersDir);
|
|
212
|
+
const handlerWatcher = watch(handlerPattern, {
|
|
213
|
+
cwd: absoluteHandlersDir,
|
|
214
|
+
ignoreInitial: true,
|
|
215
|
+
ignored: ["**/node_modules/**", "**/dist/**"],
|
|
216
|
+
persistent: true,
|
|
217
|
+
awaitWriteFinish: {
|
|
218
|
+
stabilityThreshold: 100,
|
|
219
|
+
pollInterval: 50
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
handlerWatcher.on("add", (file) => {
|
|
223
|
+
const absolutePath = path2.join(absoluteHandlersDir, file);
|
|
224
|
+
safeInvoke(onHandlerChange, absolutePath, "Handler add");
|
|
225
|
+
});
|
|
226
|
+
handlerWatcher.on("change", (file) => {
|
|
227
|
+
const absolutePath = path2.join(absoluteHandlersDir, file);
|
|
228
|
+
safeInvoke(onHandlerChange, absolutePath, "Handler change");
|
|
229
|
+
});
|
|
230
|
+
handlerWatcher.on("unlink", (file) => {
|
|
231
|
+
const absolutePath = path2.join(absoluteHandlersDir, file);
|
|
232
|
+
safeInvoke(onHandlerChange, absolutePath, "Handler unlink");
|
|
233
|
+
});
|
|
234
|
+
handlerWatcher.on("error", (error) => {
|
|
235
|
+
logger.error("[vite-plugin-open-api-server] Handler watcher error:", error);
|
|
236
|
+
});
|
|
237
|
+
readyPromises.push(
|
|
238
|
+
new Promise((resolve) => {
|
|
239
|
+
handlerWatcher.on("ready", () => resolve());
|
|
240
|
+
})
|
|
241
|
+
);
|
|
242
|
+
watchers.push(handlerWatcher);
|
|
243
|
+
}
|
|
244
|
+
if (seedsDir && onSeedChange) {
|
|
245
|
+
const absoluteSeedsDir = path2.resolve(cwd, seedsDir);
|
|
246
|
+
const seedWatcher = watch(seedPattern, {
|
|
247
|
+
cwd: absoluteSeedsDir,
|
|
248
|
+
ignoreInitial: true,
|
|
249
|
+
ignored: ["**/node_modules/**", "**/dist/**"],
|
|
250
|
+
persistent: true,
|
|
251
|
+
awaitWriteFinish: {
|
|
252
|
+
stabilityThreshold: 100,
|
|
253
|
+
pollInterval: 50
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
seedWatcher.on("add", (file) => {
|
|
257
|
+
const absolutePath = path2.join(absoluteSeedsDir, file);
|
|
258
|
+
safeInvoke(onSeedChange, absolutePath, "Seed add");
|
|
259
|
+
});
|
|
260
|
+
seedWatcher.on("change", (file) => {
|
|
261
|
+
const absolutePath = path2.join(absoluteSeedsDir, file);
|
|
262
|
+
safeInvoke(onSeedChange, absolutePath, "Seed change");
|
|
263
|
+
});
|
|
264
|
+
seedWatcher.on("unlink", (file) => {
|
|
265
|
+
const absolutePath = path2.join(absoluteSeedsDir, file);
|
|
266
|
+
safeInvoke(onSeedChange, absolutePath, "Seed unlink");
|
|
267
|
+
});
|
|
268
|
+
seedWatcher.on("error", (error) => {
|
|
269
|
+
logger.error("[vite-plugin-open-api-server] Seed watcher error:", error);
|
|
270
|
+
});
|
|
271
|
+
readyPromises.push(
|
|
272
|
+
new Promise((resolve) => {
|
|
273
|
+
seedWatcher.on("ready", () => resolve());
|
|
274
|
+
})
|
|
275
|
+
);
|
|
276
|
+
watchers.push(seedWatcher);
|
|
277
|
+
}
|
|
278
|
+
const readyPromise = Promise.all(readyPromises).then(() => {
|
|
279
|
+
});
|
|
280
|
+
return {
|
|
281
|
+
async close() {
|
|
282
|
+
isWatching = false;
|
|
283
|
+
await Promise.all(watchers.map((w) => w.close()));
|
|
284
|
+
},
|
|
285
|
+
get isWatching() {
|
|
286
|
+
return isWatching;
|
|
287
|
+
},
|
|
288
|
+
get ready() {
|
|
289
|
+
return readyPromise;
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
function debounce(fn, delay) {
|
|
294
|
+
let timeoutId = null;
|
|
295
|
+
let isRunning = false;
|
|
296
|
+
let pendingArgs = null;
|
|
297
|
+
const execute = async (...args) => {
|
|
298
|
+
if (isRunning) {
|
|
299
|
+
pendingArgs = args;
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
isRunning = true;
|
|
303
|
+
try {
|
|
304
|
+
try {
|
|
305
|
+
await fn(...args);
|
|
306
|
+
} catch {
|
|
307
|
+
}
|
|
308
|
+
} finally {
|
|
309
|
+
isRunning = false;
|
|
310
|
+
if (pendingArgs !== null) {
|
|
311
|
+
const nextArgs = pendingArgs;
|
|
312
|
+
pendingArgs = null;
|
|
313
|
+
setTimeout(() => execute(...nextArgs), 0);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
return (...args) => {
|
|
318
|
+
if (timeoutId !== null) {
|
|
319
|
+
clearTimeout(timeoutId);
|
|
320
|
+
}
|
|
321
|
+
timeoutId = setTimeout(() => {
|
|
322
|
+
timeoutId = null;
|
|
323
|
+
execute(...args);
|
|
324
|
+
}, delay);
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
async function loadSeeds(seedsDir, viteServer, cwd = process.cwd(), logger = console) {
|
|
328
|
+
const seeds = /* @__PURE__ */ new Map();
|
|
329
|
+
const absoluteDir = path2.resolve(cwd, seedsDir);
|
|
330
|
+
const dirExists = await directoryExists(absoluteDir);
|
|
331
|
+
if (!dirExists) {
|
|
332
|
+
return {
|
|
333
|
+
seeds,
|
|
334
|
+
fileCount: 0,
|
|
335
|
+
files: []
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
const pattern = "**/*.seeds.{ts,js,mjs}";
|
|
339
|
+
const files = await fg(pattern, {
|
|
340
|
+
cwd: absoluteDir,
|
|
341
|
+
absolute: false,
|
|
342
|
+
onlyFiles: true,
|
|
343
|
+
ignore: ["node_modules/**", "dist/**"]
|
|
344
|
+
});
|
|
345
|
+
for (const file of files) {
|
|
346
|
+
const absolutePath = path2.join(absoluteDir, file);
|
|
347
|
+
const fileSeeds = await loadSeedFile(absolutePath, viteServer, logger);
|
|
348
|
+
for (const [schemaName, seedFn] of Object.entries(fileSeeds)) {
|
|
349
|
+
if (seeds.has(schemaName)) {
|
|
350
|
+
logger.warn(
|
|
351
|
+
`[vite-plugin-open-api-server] Duplicate seed for schema "${schemaName}" in ${file}. Using last definition.`
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
seeds.set(schemaName, seedFn);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return {
|
|
358
|
+
seeds,
|
|
359
|
+
fileCount: files.length,
|
|
360
|
+
files
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
async function loadSeedFile(filePath, viteServer, logger) {
|
|
364
|
+
try {
|
|
365
|
+
const moduleNode = viteServer.moduleGraph.getModuleById(filePath);
|
|
366
|
+
if (moduleNode) {
|
|
367
|
+
viteServer.moduleGraph.invalidateModule(moduleNode);
|
|
368
|
+
}
|
|
369
|
+
const module = await viteServer.ssrLoadModule(filePath);
|
|
370
|
+
const seeds = module.default ?? module.seeds ?? module;
|
|
371
|
+
if (!seeds || typeof seeds !== "object") {
|
|
372
|
+
logger.warn(
|
|
373
|
+
`[vite-plugin-open-api-server] Invalid seed file ${filePath}: expected object export`
|
|
374
|
+
);
|
|
375
|
+
return {};
|
|
376
|
+
}
|
|
377
|
+
const validSeeds = {};
|
|
378
|
+
for (const [key, value] of Object.entries(seeds)) {
|
|
379
|
+
if (typeof value === "function") {
|
|
380
|
+
validSeeds[key] = value;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return validSeeds;
|
|
384
|
+
} catch (error) {
|
|
385
|
+
logger.error(
|
|
386
|
+
`[vite-plugin-open-api-server] Failed to load seed file ${filePath}:`,
|
|
387
|
+
error instanceof Error ? error.message : error
|
|
388
|
+
);
|
|
389
|
+
return {};
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
async function getSeedFiles(seedsDir, cwd = process.cwd()) {
|
|
393
|
+
const absoluteDir = path2.resolve(cwd, seedsDir);
|
|
394
|
+
const dirExists = await directoryExists(absoluteDir);
|
|
395
|
+
if (!dirExists) {
|
|
396
|
+
return [];
|
|
397
|
+
}
|
|
398
|
+
const pattern = "**/*.seeds.{ts,js,mjs}";
|
|
399
|
+
const files = await fg(pattern, {
|
|
400
|
+
cwd: absoluteDir,
|
|
401
|
+
absolute: true,
|
|
402
|
+
onlyFiles: true,
|
|
403
|
+
ignore: ["node_modules/**", "dist/**"]
|
|
404
|
+
});
|
|
405
|
+
return files;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// src/types.ts
|
|
409
|
+
function resolveOptions(options) {
|
|
410
|
+
if (!options.spec || typeof options.spec !== "string" || options.spec.trim() === "") {
|
|
411
|
+
throw new Error(
|
|
412
|
+
"spec is required and must be a non-empty string (path or URL to OpenAPI spec)"
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
return {
|
|
416
|
+
spec: options.spec,
|
|
417
|
+
port: options.port ?? 4e3,
|
|
418
|
+
proxyPath: options.proxyPath ?? "/api",
|
|
419
|
+
handlersDir: options.handlersDir ?? "./mocks/handlers",
|
|
420
|
+
seedsDir: options.seedsDir ?? "./mocks/seeds",
|
|
421
|
+
enabled: options.enabled ?? true,
|
|
422
|
+
idFields: options.idFields ?? {},
|
|
423
|
+
timelineLimit: options.timelineLimit ?? 500,
|
|
424
|
+
devtools: options.devtools ?? true,
|
|
425
|
+
cors: options.cors ?? true,
|
|
426
|
+
corsOrigin: options.corsOrigin ?? "*",
|
|
427
|
+
silent: options.silent ?? false,
|
|
428
|
+
logger: options.logger
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// src/plugin.ts
|
|
433
|
+
function openApiServer(options) {
|
|
434
|
+
const resolvedOptions = resolveOptions(options);
|
|
435
|
+
let server = null;
|
|
436
|
+
let vite = null;
|
|
437
|
+
let fileWatcher = null;
|
|
438
|
+
let cwd = process.cwd();
|
|
439
|
+
return {
|
|
440
|
+
name: "vite-plugin-open-api-server",
|
|
441
|
+
// Only active in dev mode
|
|
442
|
+
apply: "serve",
|
|
443
|
+
/**
|
|
444
|
+
* Configure the Vite dev server
|
|
445
|
+
*
|
|
446
|
+
* This hook is called when the dev server is created.
|
|
447
|
+
* We use it to:
|
|
448
|
+
* 1. Start the OpenAPI mock server
|
|
449
|
+
* 2. Configure the Vite proxy to forward API requests
|
|
450
|
+
* 3. Set up file watching for hot reload
|
|
451
|
+
*/
|
|
452
|
+
async configureServer(viteServer) {
|
|
453
|
+
vite = viteServer;
|
|
454
|
+
cwd = viteServer.config.root;
|
|
455
|
+
if (!resolvedOptions.enabled) {
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
try {
|
|
459
|
+
const handlersResult = await loadHandlers(resolvedOptions.handlersDir, viteServer, cwd);
|
|
460
|
+
const seedsResult = await loadSeeds(resolvedOptions.seedsDir, viteServer, cwd);
|
|
461
|
+
server = await createOpenApiServer({
|
|
462
|
+
spec: resolvedOptions.spec,
|
|
463
|
+
port: resolvedOptions.port,
|
|
464
|
+
idFields: resolvedOptions.idFields,
|
|
465
|
+
handlers: handlersResult.handlers,
|
|
466
|
+
// Seeds are populated via executeSeeds, not directly
|
|
467
|
+
seeds: /* @__PURE__ */ new Map(),
|
|
468
|
+
timelineLimit: resolvedOptions.timelineLimit,
|
|
469
|
+
cors: resolvedOptions.cors,
|
|
470
|
+
corsOrigin: resolvedOptions.corsOrigin,
|
|
471
|
+
logger: resolvedOptions.logger
|
|
472
|
+
});
|
|
473
|
+
if (seedsResult.seeds.size > 0) {
|
|
474
|
+
await executeSeeds(seedsResult.seeds, server.store, server.document);
|
|
475
|
+
}
|
|
476
|
+
await server.start();
|
|
477
|
+
configureProxy(viteServer, resolvedOptions.proxyPath, resolvedOptions.port);
|
|
478
|
+
const bannerInfo = extractBannerInfo(
|
|
479
|
+
server.registry,
|
|
480
|
+
{
|
|
481
|
+
info: {
|
|
482
|
+
title: server.document.info?.title ?? "OpenAPI Server",
|
|
483
|
+
version: server.document.info?.version ?? "1.0.0"
|
|
484
|
+
}
|
|
485
|
+
},
|
|
486
|
+
handlersResult.handlers.size,
|
|
487
|
+
seedsResult.seeds.size,
|
|
488
|
+
resolvedOptions
|
|
489
|
+
);
|
|
490
|
+
printBanner(bannerInfo, resolvedOptions);
|
|
491
|
+
await setupFileWatching();
|
|
492
|
+
} catch (error) {
|
|
493
|
+
printError("Failed to start OpenAPI mock server", error, resolvedOptions);
|
|
494
|
+
throw error;
|
|
495
|
+
}
|
|
496
|
+
},
|
|
497
|
+
/**
|
|
498
|
+
* Clean up when Vite server closes
|
|
499
|
+
*/
|
|
500
|
+
async closeBundle() {
|
|
501
|
+
await cleanup();
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Handle HMR (Hot Module Replacement)
|
|
505
|
+
*
|
|
506
|
+
* Note: This is called for all HMR updates, not just our files.
|
|
507
|
+
* We handle our own file watching separately.
|
|
508
|
+
*/
|
|
509
|
+
// handleHotUpdate is not needed - we use chokidar directly
|
|
510
|
+
};
|
|
511
|
+
function configureProxy(vite2, proxyPath, port) {
|
|
512
|
+
const serverConfig = vite2.config.server ?? {};
|
|
513
|
+
const proxyConfig = serverConfig.proxy ?? {};
|
|
514
|
+
const escapedPath = proxyPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
515
|
+
proxyConfig[proxyPath] = {
|
|
516
|
+
target: `http://localhost:${port}`,
|
|
517
|
+
changeOrigin: true,
|
|
518
|
+
// Remove the proxy path prefix when forwarding
|
|
519
|
+
rewrite: (path4) => path4.replace(new RegExp(`^${escapedPath}`), "")
|
|
520
|
+
};
|
|
521
|
+
if (vite2.config.server) {
|
|
522
|
+
vite2.config.server.proxy = proxyConfig;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
async function setupFileWatching() {
|
|
526
|
+
if (!server || !vite) return;
|
|
527
|
+
const debouncedHandlerReload = debounce(reloadHandlers, 100);
|
|
528
|
+
const debouncedSeedReload = debounce(reloadSeeds, 100);
|
|
529
|
+
fileWatcher = await createFileWatcher({
|
|
530
|
+
handlersDir: resolvedOptions.handlersDir,
|
|
531
|
+
seedsDir: resolvedOptions.seedsDir,
|
|
532
|
+
cwd,
|
|
533
|
+
onHandlerChange: debouncedHandlerReload,
|
|
534
|
+
onSeedChange: debouncedSeedReload
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
async function reloadHandlers() {
|
|
538
|
+
if (!server || !vite) return;
|
|
539
|
+
try {
|
|
540
|
+
const handlersResult = await loadHandlers(resolvedOptions.handlersDir, vite, cwd);
|
|
541
|
+
server.updateHandlers(handlersResult.handlers);
|
|
542
|
+
server.wsHub.broadcast({
|
|
543
|
+
type: "handlers:updated",
|
|
544
|
+
data: { count: handlersResult.handlers.size }
|
|
545
|
+
});
|
|
546
|
+
printReloadNotification("handlers", handlersResult.handlers.size, resolvedOptions);
|
|
547
|
+
} catch (error) {
|
|
548
|
+
printError("Failed to reload handlers", error, resolvedOptions);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
async function reloadSeeds() {
|
|
552
|
+
if (!server || !vite) return;
|
|
553
|
+
try {
|
|
554
|
+
const seedsResult = await loadSeeds(resolvedOptions.seedsDir, vite, cwd);
|
|
555
|
+
if (seedsResult.seeds.size > 0) {
|
|
556
|
+
server.store.clearAll();
|
|
557
|
+
await executeSeeds(seedsResult.seeds, server.store, server.document);
|
|
558
|
+
} else {
|
|
559
|
+
server.store.clearAll();
|
|
560
|
+
}
|
|
561
|
+
server.wsHub.broadcast({
|
|
562
|
+
type: "seeds:updated",
|
|
563
|
+
data: { count: seedsResult.seeds.size }
|
|
564
|
+
});
|
|
565
|
+
printReloadNotification("seeds", seedsResult.seeds.size, resolvedOptions);
|
|
566
|
+
} catch (error) {
|
|
567
|
+
printError("Failed to reload seeds", error, resolvedOptions);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
async function cleanup() {
|
|
571
|
+
if (fileWatcher) {
|
|
572
|
+
await fileWatcher.close();
|
|
573
|
+
fileWatcher = null;
|
|
574
|
+
}
|
|
575
|
+
if (server) {
|
|
576
|
+
await server.stop();
|
|
577
|
+
server = null;
|
|
578
|
+
}
|
|
579
|
+
vite = null;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
export { createFileWatcher, debounce, getHandlerFiles, getSeedFiles, loadHandlers, loadSeeds, openApiServer };
|
|
584
|
+
//# sourceMappingURL=index.js.map
|
|
585
|
+
//# sourceMappingURL=index.js.map
|