@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/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