@websublime/vite-plugin-open-api-server 0.24.0-next.0 → 0.24.0-next.10
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.d.ts +443 -23
- package/dist/index.js +1166 -302
- package/dist/index.js.map +1 -1
- package/package.json +27 -11
package/dist/index.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import { existsSync } from 'fs';
|
|
2
1
|
import { createRequire } from 'module';
|
|
3
|
-
import path2, { dirname, join } from 'path';
|
|
4
|
-
import { fileURLToPath } from 'url';
|
|
5
|
-
import { createOpenApiServer, executeSeeds } from '@websublime/vite-plugin-open-api-core';
|
|
6
|
-
export { defineHandlers, defineSeeds } from '@websublime/vite-plugin-open-api-core';
|
|
7
2
|
import pc from 'picocolors';
|
|
3
|
+
import { existsSync } from 'fs';
|
|
4
|
+
import path, { dirname, join } from 'path';
|
|
5
|
+
import { executeSeeds, createOpenApiServer, mountDevToolsRoutes, createWebSocketHub } from '@websublime/vite-plugin-open-api-core';
|
|
6
|
+
export { defineHandlers, defineSeeds } from '@websublime/vite-plugin-open-api-core';
|
|
8
7
|
import fg from 'fast-glob';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
import { Hono } from 'hono';
|
|
10
|
+
import { cors } from 'hono/cors';
|
|
9
11
|
|
|
10
12
|
// src/plugin.ts
|
|
11
13
|
function printBanner(info, options) {
|
|
@@ -109,7 +111,7 @@ async function directoryExists(dirPath) {
|
|
|
109
111
|
// src/handlers.ts
|
|
110
112
|
async function loadHandlers(handlersDir, viteServer, cwd = process.cwd(), logger = console) {
|
|
111
113
|
const handlers = /* @__PURE__ */ new Map();
|
|
112
|
-
const absoluteDir =
|
|
114
|
+
const absoluteDir = path.resolve(cwd, handlersDir);
|
|
113
115
|
const dirExists = await directoryExists(absoluteDir);
|
|
114
116
|
if (!dirExists) {
|
|
115
117
|
return {
|
|
@@ -126,7 +128,7 @@ async function loadHandlers(handlersDir, viteServer, cwd = process.cwd(), logger
|
|
|
126
128
|
ignore: ["node_modules/**", "dist/**"]
|
|
127
129
|
});
|
|
128
130
|
for (const file of files) {
|
|
129
|
-
const absolutePath =
|
|
131
|
+
const absolutePath = path.join(absoluteDir, file);
|
|
130
132
|
const fileHandlers = await loadHandlerFile(absolutePath, viteServer, logger);
|
|
131
133
|
for (const [operationId, handler] of Object.entries(fileHandlers)) {
|
|
132
134
|
if (handlers.has(operationId)) {
|
|
@@ -173,7 +175,7 @@ async function loadHandlerFile(filePath, viteServer, logger) {
|
|
|
173
175
|
}
|
|
174
176
|
}
|
|
175
177
|
async function getHandlerFiles(handlersDir, cwd = process.cwd()) {
|
|
176
|
-
const absoluteDir =
|
|
178
|
+
const absoluteDir = path.resolve(cwd, handlersDir);
|
|
177
179
|
const dirExists = await directoryExists(absoluteDir);
|
|
178
180
|
if (!dirExists) {
|
|
179
181
|
return [];
|
|
@@ -187,6 +189,90 @@ async function getHandlerFiles(handlersDir, cwd = process.cwd()) {
|
|
|
187
189
|
});
|
|
188
190
|
return files;
|
|
189
191
|
}
|
|
192
|
+
async function loadSeeds(seedsDir, viteServer, cwd = process.cwd(), logger = console) {
|
|
193
|
+
const seeds = /* @__PURE__ */ new Map();
|
|
194
|
+
const absoluteDir = path.resolve(cwd, seedsDir);
|
|
195
|
+
const dirExists = await directoryExists(absoluteDir);
|
|
196
|
+
if (!dirExists) {
|
|
197
|
+
return {
|
|
198
|
+
seeds,
|
|
199
|
+
fileCount: 0,
|
|
200
|
+
files: []
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
const pattern = "**/*.seeds.{ts,js,mjs}";
|
|
204
|
+
const files = await fg(pattern, {
|
|
205
|
+
cwd: absoluteDir,
|
|
206
|
+
absolute: false,
|
|
207
|
+
onlyFiles: true,
|
|
208
|
+
ignore: ["node_modules/**", "dist/**"]
|
|
209
|
+
});
|
|
210
|
+
for (const file of files) {
|
|
211
|
+
const absolutePath = path.join(absoluteDir, file);
|
|
212
|
+
const fileSeeds = await loadSeedFile(absolutePath, viteServer, logger);
|
|
213
|
+
for (const [schemaName, seedFn] of Object.entries(fileSeeds)) {
|
|
214
|
+
if (seeds.has(schemaName)) {
|
|
215
|
+
logger.warn(
|
|
216
|
+
`[vite-plugin-open-api-server] Duplicate seed for schema "${schemaName}" in ${file}. Using last definition.`
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
seeds.set(schemaName, seedFn);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
seeds,
|
|
224
|
+
fileCount: files.length,
|
|
225
|
+
files
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
async function loadSeedFile(filePath, viteServer, logger) {
|
|
229
|
+
try {
|
|
230
|
+
const moduleNode = viteServer.moduleGraph.getModuleById(filePath);
|
|
231
|
+
if (moduleNode) {
|
|
232
|
+
viteServer.moduleGraph.invalidateModule(moduleNode);
|
|
233
|
+
}
|
|
234
|
+
const module = await viteServer.ssrLoadModule(filePath);
|
|
235
|
+
const seeds = module.default ?? module.seeds ?? module;
|
|
236
|
+
if (!seeds || typeof seeds !== "object") {
|
|
237
|
+
logger.warn(
|
|
238
|
+
`[vite-plugin-open-api-server] Invalid seed file ${filePath}: expected object export`
|
|
239
|
+
);
|
|
240
|
+
return {};
|
|
241
|
+
}
|
|
242
|
+
const validSeeds = {};
|
|
243
|
+
for (const [key, value] of Object.entries(seeds)) {
|
|
244
|
+
if (typeof value === "function") {
|
|
245
|
+
validSeeds[key] = value;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return validSeeds;
|
|
249
|
+
} catch (error) {
|
|
250
|
+
logger.error(
|
|
251
|
+
`[vite-plugin-open-api-server] Failed to load seed file ${filePath}:`,
|
|
252
|
+
error instanceof Error ? error.message : error
|
|
253
|
+
);
|
|
254
|
+
return {};
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
async function getSeedFiles(seedsDir, cwd = process.cwd()) {
|
|
258
|
+
const absoluteDir = path.resolve(cwd, seedsDir);
|
|
259
|
+
const dirExists = await directoryExists(absoluteDir);
|
|
260
|
+
if (!dirExists) {
|
|
261
|
+
return [];
|
|
262
|
+
}
|
|
263
|
+
const pattern = "**/*.seeds.{ts,js,mjs}";
|
|
264
|
+
const files = await fg(pattern, {
|
|
265
|
+
cwd: absoluteDir,
|
|
266
|
+
absolute: true,
|
|
267
|
+
onlyFiles: true,
|
|
268
|
+
ignore: ["node_modules/**", "dist/**"]
|
|
269
|
+
});
|
|
270
|
+
return files;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// src/hot-reload.ts
|
|
274
|
+
var nodeModulesRe = /(^|[\\/])node_modules([\\/]|$)/;
|
|
275
|
+
var distRe = /(^|[\\/])dist([\\/]|$)/;
|
|
190
276
|
async function createFileWatcher(options) {
|
|
191
277
|
const {
|
|
192
278
|
handlersDir,
|
|
@@ -200,8 +286,8 @@ async function createFileWatcher(options) {
|
|
|
200
286
|
const watchers = [];
|
|
201
287
|
const readyPromises = [];
|
|
202
288
|
let isWatching = true;
|
|
203
|
-
const
|
|
204
|
-
const
|
|
289
|
+
const handlerRe = /\.handlers\.(ts|js|mjs)$/;
|
|
290
|
+
const seedRe = /\.seeds\.(ts|js|mjs)$/;
|
|
205
291
|
const safeInvoke = (callback, filePath, context) => {
|
|
206
292
|
Promise.resolve().then(() => callback(filePath)).catch((error) => {
|
|
207
293
|
logger.error(
|
|
@@ -210,80 +296,103 @@ async function createFileWatcher(options) {
|
|
|
210
296
|
);
|
|
211
297
|
});
|
|
212
298
|
};
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
ignoreInitial: true,
|
|
218
|
-
ignored: ["**/node_modules/**", "**/dist/**"],
|
|
219
|
-
persistent: true,
|
|
220
|
-
awaitWriteFinish: {
|
|
221
|
-
stabilityThreshold: 100,
|
|
222
|
-
pollInterval: 50
|
|
299
|
+
const buildIgnored = (pattern) => {
|
|
300
|
+
return (filePath, stats) => {
|
|
301
|
+
if (nodeModulesRe.test(filePath) || distRe.test(filePath)) {
|
|
302
|
+
return true;
|
|
223
303
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const absolutePath = path2.join(absoluteHandlersDir, file);
|
|
227
|
-
safeInvoke(onHandlerChange, absolutePath, "Handler add");
|
|
228
|
-
});
|
|
229
|
-
handlerWatcher.on("change", (file) => {
|
|
230
|
-
const absolutePath = path2.join(absoluteHandlersDir, file);
|
|
231
|
-
safeInvoke(onHandlerChange, absolutePath, "Handler change");
|
|
232
|
-
});
|
|
233
|
-
handlerWatcher.on("unlink", (file) => {
|
|
234
|
-
const absolutePath = path2.join(absoluteHandlersDir, file);
|
|
235
|
-
safeInvoke(onHandlerChange, absolutePath, "Handler unlink");
|
|
236
|
-
});
|
|
237
|
-
handlerWatcher.on("error", (error) => {
|
|
238
|
-
logger.error("[vite-plugin-open-api-server] Handler watcher error:", error);
|
|
239
|
-
});
|
|
240
|
-
readyPromises.push(
|
|
241
|
-
new Promise((resolve) => {
|
|
242
|
-
handlerWatcher.on("ready", () => resolve());
|
|
243
|
-
})
|
|
244
|
-
);
|
|
245
|
-
watchers.push(handlerWatcher);
|
|
246
|
-
}
|
|
247
|
-
if (seedsDir && onSeedChange) {
|
|
248
|
-
const absoluteSeedsDir = path2.resolve(cwd, seedsDir);
|
|
249
|
-
const seedWatcher = watch(seedPattern, {
|
|
250
|
-
cwd: absoluteSeedsDir,
|
|
251
|
-
ignoreInitial: true,
|
|
252
|
-
ignored: ["**/node_modules/**", "**/dist/**"],
|
|
253
|
-
persistent: true,
|
|
254
|
-
awaitWriteFinish: {
|
|
255
|
-
stabilityThreshold: 100,
|
|
256
|
-
pollInterval: 50
|
|
304
|
+
if (!stats) {
|
|
305
|
+
return false;
|
|
257
306
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
307
|
+
if (!stats.isFile()) {
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
return !pattern.test(filePath);
|
|
311
|
+
};
|
|
312
|
+
};
|
|
313
|
+
try {
|
|
314
|
+
if (handlersDir && onHandlerChange) {
|
|
315
|
+
const absoluteHandlersDir = path.resolve(cwd, handlersDir);
|
|
316
|
+
if (!existsSync(absoluteHandlersDir)) {
|
|
317
|
+
logger.warn(
|
|
318
|
+
`[vite-plugin-open-api-server] Handlers directory does not exist, skipping watcher: ${absoluteHandlersDir}`
|
|
319
|
+
);
|
|
320
|
+
} else {
|
|
321
|
+
const handlerWatcher = watch(absoluteHandlersDir, {
|
|
322
|
+
ignoreInitial: true,
|
|
323
|
+
ignored: buildIgnored(handlerRe),
|
|
324
|
+
persistent: true,
|
|
325
|
+
awaitWriteFinish: {
|
|
326
|
+
stabilityThreshold: 100,
|
|
327
|
+
pollInterval: 50
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
handlerWatcher.on("add", (file) => {
|
|
331
|
+
safeInvoke(onHandlerChange, file, "Handler add");
|
|
332
|
+
});
|
|
333
|
+
handlerWatcher.on("change", (file) => {
|
|
334
|
+
safeInvoke(onHandlerChange, file, "Handler change");
|
|
335
|
+
});
|
|
336
|
+
handlerWatcher.on("unlink", (file) => {
|
|
337
|
+
safeInvoke(onHandlerChange, file, "Handler unlink");
|
|
338
|
+
});
|
|
339
|
+
handlerWatcher.on("error", (error) => {
|
|
340
|
+
logger.error("[vite-plugin-open-api-server] Handler watcher error:", error);
|
|
341
|
+
});
|
|
342
|
+
readyPromises.push(
|
|
343
|
+
new Promise((resolve) => {
|
|
344
|
+
handlerWatcher.on("ready", () => resolve());
|
|
345
|
+
})
|
|
346
|
+
);
|
|
347
|
+
watchers.push(handlerWatcher);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
if (seedsDir && onSeedChange) {
|
|
351
|
+
const absoluteSeedsDir = path.resolve(cwd, seedsDir);
|
|
352
|
+
if (!existsSync(absoluteSeedsDir)) {
|
|
353
|
+
logger.warn(
|
|
354
|
+
`[vite-plugin-open-api-server] Seeds directory does not exist, skipping watcher: ${absoluteSeedsDir}`
|
|
355
|
+
);
|
|
356
|
+
} else {
|
|
357
|
+
const seedWatcher = watch(absoluteSeedsDir, {
|
|
358
|
+
ignoreInitial: true,
|
|
359
|
+
ignored: buildIgnored(seedRe),
|
|
360
|
+
persistent: true,
|
|
361
|
+
awaitWriteFinish: {
|
|
362
|
+
stabilityThreshold: 100,
|
|
363
|
+
pollInterval: 50
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
seedWatcher.on("add", (file) => {
|
|
367
|
+
safeInvoke(onSeedChange, file, "Seed add");
|
|
368
|
+
});
|
|
369
|
+
seedWatcher.on("change", (file) => {
|
|
370
|
+
safeInvoke(onSeedChange, file, "Seed change");
|
|
371
|
+
});
|
|
372
|
+
seedWatcher.on("unlink", (file) => {
|
|
373
|
+
safeInvoke(onSeedChange, file, "Seed unlink");
|
|
374
|
+
});
|
|
375
|
+
seedWatcher.on("error", (error) => {
|
|
376
|
+
logger.error("[vite-plugin-open-api-server] Seed watcher error:", error);
|
|
377
|
+
});
|
|
378
|
+
readyPromises.push(
|
|
379
|
+
new Promise((resolve) => {
|
|
380
|
+
seedWatcher.on("ready", () => resolve());
|
|
381
|
+
})
|
|
382
|
+
);
|
|
383
|
+
watchers.push(seedWatcher);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
} catch (error) {
|
|
387
|
+
await Promise.allSettled(watchers.map((w) => w.close()));
|
|
388
|
+
throw error;
|
|
280
389
|
}
|
|
281
390
|
const readyPromise = Promise.all(readyPromises).then(() => {
|
|
282
391
|
});
|
|
283
392
|
return {
|
|
284
393
|
async close() {
|
|
285
394
|
isWatching = false;
|
|
286
|
-
await Promise.
|
|
395
|
+
await Promise.allSettled(watchers.map((w) => w.close()));
|
|
287
396
|
},
|
|
288
397
|
get isWatching() {
|
|
289
398
|
return isWatching;
|
|
@@ -297,9 +406,12 @@ function debounce(fn, delay) {
|
|
|
297
406
|
let timeoutId = null;
|
|
298
407
|
let isRunning = false;
|
|
299
408
|
let pendingArgs = null;
|
|
409
|
+
let cancelled = false;
|
|
300
410
|
const execute = async (...args) => {
|
|
301
|
-
if (isRunning) {
|
|
302
|
-
|
|
411
|
+
if (cancelled || isRunning) {
|
|
412
|
+
if (isRunning && !cancelled) {
|
|
413
|
+
pendingArgs = args;
|
|
414
|
+
}
|
|
303
415
|
return;
|
|
304
416
|
}
|
|
305
417
|
isRunning = true;
|
|
@@ -310,14 +422,15 @@ function debounce(fn, delay) {
|
|
|
310
422
|
}
|
|
311
423
|
} finally {
|
|
312
424
|
isRunning = false;
|
|
313
|
-
if (pendingArgs !== null) {
|
|
425
|
+
if (pendingArgs !== null && !cancelled) {
|
|
314
426
|
const nextArgs = pendingArgs;
|
|
315
427
|
pendingArgs = null;
|
|
316
428
|
setTimeout(() => execute(...nextArgs), 0);
|
|
317
429
|
}
|
|
318
430
|
}
|
|
319
431
|
};
|
|
320
|
-
|
|
432
|
+
const debouncedFn = (...args) => {
|
|
433
|
+
if (cancelled) return;
|
|
321
434
|
if (timeoutId !== null) {
|
|
322
435
|
clearTimeout(timeoutId);
|
|
323
436
|
}
|
|
@@ -326,86 +439,478 @@ function debounce(fn, delay) {
|
|
|
326
439
|
execute(...args);
|
|
327
440
|
}, delay);
|
|
328
441
|
};
|
|
442
|
+
debouncedFn.cancel = () => {
|
|
443
|
+
cancelled = true;
|
|
444
|
+
if (timeoutId !== null) {
|
|
445
|
+
clearTimeout(timeoutId);
|
|
446
|
+
timeoutId = null;
|
|
447
|
+
}
|
|
448
|
+
pendingArgs = null;
|
|
449
|
+
};
|
|
450
|
+
return debouncedFn;
|
|
329
451
|
}
|
|
330
|
-
async function
|
|
331
|
-
const
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
452
|
+
async function createPerSpecFileWatchers(instances, vite, cwd, options) {
|
|
453
|
+
const watchers = [];
|
|
454
|
+
const allDebouncedFns = [];
|
|
455
|
+
try {
|
|
456
|
+
for (const instance of instances) {
|
|
457
|
+
const debouncedHandlerReload = debounce(
|
|
458
|
+
() => reloadSpecHandlers(instance, vite, cwd, options),
|
|
459
|
+
100
|
|
460
|
+
);
|
|
461
|
+
const debouncedSeedReload = debounce(
|
|
462
|
+
() => reloadSpecSeeds(instance, vite, cwd, options),
|
|
463
|
+
100
|
|
464
|
+
);
|
|
465
|
+
allDebouncedFns.push(debouncedHandlerReload, debouncedSeedReload);
|
|
466
|
+
const innerWatcher = await createFileWatcher({
|
|
467
|
+
handlersDir: instance.config.handlersDir,
|
|
468
|
+
seedsDir: instance.config.seedsDir,
|
|
469
|
+
cwd,
|
|
470
|
+
logger: options.logger,
|
|
471
|
+
onHandlerChange: debouncedHandlerReload,
|
|
472
|
+
onSeedChange: debouncedSeedReload
|
|
473
|
+
});
|
|
474
|
+
watchers.push({
|
|
475
|
+
async close() {
|
|
476
|
+
debouncedHandlerReload.cancel();
|
|
477
|
+
debouncedSeedReload.cancel();
|
|
478
|
+
await innerWatcher.close();
|
|
479
|
+
},
|
|
480
|
+
get isWatching() {
|
|
481
|
+
return innerWatcher.isWatching;
|
|
482
|
+
},
|
|
483
|
+
get ready() {
|
|
484
|
+
return innerWatcher.ready;
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
} catch (error) {
|
|
489
|
+
for (const fn of allDebouncedFns) fn.cancel();
|
|
490
|
+
await Promise.allSettled(watchers.map((w) => w.close()));
|
|
491
|
+
throw error;
|
|
340
492
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
493
|
+
return watchers;
|
|
494
|
+
}
|
|
495
|
+
async function reloadSpecHandlers(instance, vite, cwd, options) {
|
|
496
|
+
try {
|
|
497
|
+
const logger = options.logger ?? console;
|
|
498
|
+
const handlersResult = await loadHandlers(instance.config.handlersDir, vite, cwd, logger);
|
|
499
|
+
instance.server.updateHandlers(handlersResult.handlers);
|
|
500
|
+
printReloadNotification("handlers", handlersResult.handlers.size, options);
|
|
501
|
+
} catch (error) {
|
|
502
|
+
printError(`Failed to reload handlers for spec "${instance.id}"`, error, options);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
async function reloadSpecSeeds(instance, vite, cwd, options) {
|
|
506
|
+
try {
|
|
507
|
+
const logger = options.logger ?? console;
|
|
508
|
+
const seedsResult = await loadSeeds(instance.config.seedsDir, vite, cwd, logger);
|
|
509
|
+
let broadcastCount = seedsResult.seeds.size;
|
|
510
|
+
instance.server.store.clearAll();
|
|
511
|
+
if (seedsResult.seeds.size > 0) {
|
|
512
|
+
try {
|
|
513
|
+
await executeSeeds(seedsResult.seeds, instance.server.store, instance.server.document);
|
|
514
|
+
} catch (execError) {
|
|
515
|
+
broadcastCount = 0;
|
|
516
|
+
printError(
|
|
517
|
+
`Seeds loaded but executeSeeds failed for spec "${instance.id}"; store is now empty`,
|
|
518
|
+
execError,
|
|
519
|
+
options
|
|
355
520
|
);
|
|
356
521
|
}
|
|
357
|
-
seeds.set(schemaName, seedFn);
|
|
358
522
|
}
|
|
523
|
+
instance.server.wsHub.broadcast({
|
|
524
|
+
type: "seeds:updated",
|
|
525
|
+
data: { count: broadcastCount }
|
|
526
|
+
});
|
|
527
|
+
if (broadcastCount > 0) {
|
|
528
|
+
printReloadNotification("seeds", broadcastCount, options);
|
|
529
|
+
}
|
|
530
|
+
} catch (error) {
|
|
531
|
+
printError(`Failed to reload seeds for spec "${instance.id}"`, error, options);
|
|
359
532
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// src/multi-proxy.ts
|
|
536
|
+
var DEVTOOLS_PROXY_PATH = "/_devtools";
|
|
537
|
+
var API_PROXY_PATH = "/_api";
|
|
538
|
+
var WS_PROXY_PATH = "/_ws";
|
|
539
|
+
function getProxyConfig(vite) {
|
|
540
|
+
if (!vite.config.server) {
|
|
541
|
+
return null;
|
|
542
|
+
}
|
|
543
|
+
vite.config.server.proxy ??= {};
|
|
544
|
+
return vite.config.server.proxy;
|
|
545
|
+
}
|
|
546
|
+
function configureMultiProxy(vite, instances, port) {
|
|
547
|
+
const proxyConfig = getProxyConfig(vite);
|
|
548
|
+
if (!proxyConfig) {
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
const httpTarget = `http://localhost:${port}`;
|
|
552
|
+
for (const instance of instances) {
|
|
553
|
+
const prefix = instance.config.proxyPath;
|
|
554
|
+
proxyConfig[prefix] = {
|
|
555
|
+
target: httpTarget,
|
|
556
|
+
changeOrigin: true,
|
|
557
|
+
rewrite: (path4) => {
|
|
558
|
+
if (path4 !== prefix && !path4.startsWith(`${prefix}/`) && !path4.startsWith(`${prefix}?`))
|
|
559
|
+
return path4;
|
|
560
|
+
const rest = path4.slice(prefix.length);
|
|
561
|
+
if (rest === "" || rest === "/") return "/";
|
|
562
|
+
if (rest.startsWith("?")) return `/${rest}`;
|
|
563
|
+
return rest;
|
|
564
|
+
},
|
|
565
|
+
headers: { "x-spec-id": instance.id }
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
proxyConfig[DEVTOOLS_PROXY_PATH] = {
|
|
569
|
+
target: httpTarget,
|
|
570
|
+
changeOrigin: true
|
|
571
|
+
};
|
|
572
|
+
proxyConfig[API_PROXY_PATH] = {
|
|
573
|
+
target: httpTarget,
|
|
574
|
+
changeOrigin: true
|
|
575
|
+
};
|
|
576
|
+
proxyConfig[WS_PROXY_PATH] = {
|
|
577
|
+
target: `ws://localhost:${port}`,
|
|
578
|
+
changeOrigin: true,
|
|
579
|
+
ws: true
|
|
364
580
|
};
|
|
365
581
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
582
|
+
|
|
583
|
+
// package.json
|
|
584
|
+
var package_default = {
|
|
585
|
+
version: "0.24.0-next.9"};
|
|
586
|
+
|
|
587
|
+
// src/multi-internal-api.ts
|
|
588
|
+
var PACKAGE_VERSION = package_default.version;
|
|
589
|
+
function mountMultiSpecInternalApi(app, instances) {
|
|
590
|
+
const seen = /* @__PURE__ */ new Set();
|
|
591
|
+
for (const inst of instances) {
|
|
592
|
+
if (seen.has(inst.id)) {
|
|
593
|
+
throw new Error(
|
|
594
|
+
`[vite-plugin-open-api-server] Duplicate specId "${inst.id}" in instances array`
|
|
377
595
|
);
|
|
378
|
-
return {};
|
|
379
596
|
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
597
|
+
seen.add(inst.id);
|
|
598
|
+
}
|
|
599
|
+
const instanceMap = new Map(instances.map((i) => [i.id, i]));
|
|
600
|
+
app.get("/_api/specs", (c) => {
|
|
601
|
+
const specs = instances.map((i) => ({
|
|
602
|
+
id: i.id,
|
|
603
|
+
title: i.info.title,
|
|
604
|
+
version: i.info.version,
|
|
605
|
+
proxyPath: i.config.proxyPath,
|
|
606
|
+
color: i.info.color,
|
|
607
|
+
endpoints: i.server.registry.endpoints.size,
|
|
608
|
+
schemas: i.server.store.getSchemas().length,
|
|
609
|
+
simulations: i.server.simulationManager.count()
|
|
610
|
+
}));
|
|
611
|
+
return c.json({ specs, count: specs.length });
|
|
612
|
+
});
|
|
613
|
+
app.get("/_api/registry", (c) => {
|
|
614
|
+
const registries = instances.map((i) => ({
|
|
615
|
+
specId: i.id,
|
|
616
|
+
specTitle: i.info.title,
|
|
617
|
+
specColor: i.info.color,
|
|
618
|
+
endpoints: Array.from(i.server.registry.endpoints.entries()).map(([key, entry]) => ({
|
|
619
|
+
...entry,
|
|
620
|
+
key
|
|
621
|
+
})),
|
|
622
|
+
stats: i.server.registry.stats
|
|
623
|
+
}));
|
|
624
|
+
const totalEndpoints = registries.reduce((sum, r) => sum + r.endpoints.length, 0);
|
|
625
|
+
return c.json({
|
|
626
|
+
specs: registries,
|
|
627
|
+
totalEndpoints,
|
|
628
|
+
totalSpecs: registries.length
|
|
629
|
+
});
|
|
630
|
+
});
|
|
631
|
+
app.get("/_api/health", (c) => {
|
|
632
|
+
const specs = instances.map((i) => ({
|
|
633
|
+
id: i.id,
|
|
634
|
+
endpoints: i.server.registry.endpoints.size,
|
|
635
|
+
schemas: i.server.store.getSchemas().length,
|
|
636
|
+
simulations: i.server.simulationManager.count()
|
|
637
|
+
}));
|
|
638
|
+
return c.json({
|
|
639
|
+
status: "ok",
|
|
640
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
641
|
+
version: PACKAGE_VERSION,
|
|
642
|
+
totalSpecs: instances.length,
|
|
643
|
+
totalEndpoints: specs.reduce((s, i) => s + i.endpoints, 0),
|
|
644
|
+
specs
|
|
645
|
+
});
|
|
646
|
+
});
|
|
647
|
+
const specApi = new Hono();
|
|
648
|
+
specApi.use("/:specId/*", async (c, next) => {
|
|
649
|
+
const specId = c.req.param("specId");
|
|
650
|
+
const instance = instanceMap.get(specId);
|
|
651
|
+
if (!instance) {
|
|
652
|
+
return c.json({ error: `Unknown spec: ${specId}` }, 404);
|
|
653
|
+
}
|
|
654
|
+
c.set("specInstance", instance);
|
|
655
|
+
await next();
|
|
656
|
+
});
|
|
657
|
+
specApi.get("/:specId/registry", (c) => {
|
|
658
|
+
const instance = c.get("specInstance");
|
|
659
|
+
return c.json({
|
|
660
|
+
specId: instance.id,
|
|
661
|
+
endpoints: Array.from(instance.server.registry.endpoints.entries()).map(([key, entry]) => ({
|
|
662
|
+
...entry,
|
|
663
|
+
key
|
|
664
|
+
})),
|
|
665
|
+
stats: instance.server.registry.stats
|
|
666
|
+
});
|
|
667
|
+
});
|
|
668
|
+
specApi.get("/:specId/store", (c) => {
|
|
669
|
+
const instance = c.get("specInstance");
|
|
670
|
+
const schemas = instance.server.store.getSchemas().map((schema) => ({
|
|
671
|
+
name: schema,
|
|
672
|
+
count: instance.server.store.getCount(schema),
|
|
673
|
+
idField: instance.server.store.getIdField(schema)
|
|
674
|
+
}));
|
|
675
|
+
return c.json({ specId: instance.id, schemas });
|
|
676
|
+
});
|
|
677
|
+
specApi.get("/:specId/store/:schema", (c) => {
|
|
678
|
+
const instance = c.get("specInstance");
|
|
679
|
+
const schema = c.req.param("schema");
|
|
680
|
+
const allItems = instance.server.store.list(schema);
|
|
681
|
+
const total = allItems.length;
|
|
682
|
+
const rawOffset = Number(c.req.query("offset"));
|
|
683
|
+
const offset = Number.isFinite(rawOffset) ? Math.max(Math.floor(rawOffset), 0) : 0;
|
|
684
|
+
const rawLimit = Number(c.req.query("limit"));
|
|
685
|
+
const limit = Number.isFinite(rawLimit) ? Math.min(Math.max(Math.floor(rawLimit), 0), 1e3) : Math.min(total, 1e3);
|
|
686
|
+
const items = limit === 0 ? [] : allItems.slice(offset, offset + limit);
|
|
687
|
+
return c.json({
|
|
688
|
+
specId: instance.id,
|
|
689
|
+
schema,
|
|
690
|
+
idField: instance.server.store.getIdField(schema),
|
|
691
|
+
items,
|
|
692
|
+
count: items.length,
|
|
693
|
+
total,
|
|
694
|
+
offset,
|
|
695
|
+
limit
|
|
696
|
+
});
|
|
697
|
+
});
|
|
698
|
+
specApi.get("/:specId/document", (c) => {
|
|
699
|
+
const instance = c.get("specInstance");
|
|
700
|
+
return c.json(instance.server.document);
|
|
701
|
+
});
|
|
702
|
+
specApi.get("/:specId/simulations", (c) => {
|
|
703
|
+
const instance = c.get("specInstance");
|
|
704
|
+
return c.json({
|
|
705
|
+
specId: instance.id,
|
|
706
|
+
simulations: instance.server.simulationManager.list(),
|
|
707
|
+
count: instance.server.simulationManager.count()
|
|
708
|
+
});
|
|
709
|
+
});
|
|
710
|
+
specApi.get("/:specId/timeline", (c) => {
|
|
711
|
+
const instance = c.get("specInstance");
|
|
712
|
+
const parsed = Number(c.req.query("limit"));
|
|
713
|
+
const limit = Number.isFinite(parsed) ? Math.min(Math.max(Math.floor(parsed), 0), 1e3) : 100;
|
|
714
|
+
const timeline = instance.server.getTimeline();
|
|
715
|
+
const entries = limit === 0 ? [] : timeline.slice(-limit);
|
|
716
|
+
return c.json({
|
|
717
|
+
specId: instance.id,
|
|
718
|
+
entries,
|
|
719
|
+
count: entries.length,
|
|
720
|
+
total: timeline.length,
|
|
721
|
+
limit
|
|
722
|
+
});
|
|
723
|
+
});
|
|
724
|
+
app.route("/_api/specs", specApi);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// src/multi-command.ts
|
|
728
|
+
var SPEC_SCOPED_COMMANDS = /* @__PURE__ */ new Set([
|
|
729
|
+
"get:store",
|
|
730
|
+
"set:store",
|
|
731
|
+
"clear:store",
|
|
732
|
+
"set:simulation",
|
|
733
|
+
"clear:simulation",
|
|
734
|
+
"reseed"
|
|
735
|
+
]);
|
|
736
|
+
function sendError(hub, client, command, message) {
|
|
737
|
+
hub.sendTo(client, { type: "error", data: { command, message } });
|
|
738
|
+
}
|
|
739
|
+
function resolveInstance(specId, instanceMap, hub, client, commandType) {
|
|
740
|
+
if (!specId) return void 0;
|
|
741
|
+
const instance = instanceMap.get(specId);
|
|
742
|
+
if (!instance) {
|
|
743
|
+
sendError(hub, client, commandType, `Unknown spec: ${specId}`);
|
|
744
|
+
}
|
|
745
|
+
return instance;
|
|
746
|
+
}
|
|
747
|
+
function handleGetSpecs(hub, client, specsInfo, serverVersion) {
|
|
748
|
+
hub.sendTo(client, { type: "connected", data: { serverVersion, specs: specsInfo } });
|
|
749
|
+
}
|
|
750
|
+
function handleGetRegistry(cmd, hub, client, instances, instanceMap) {
|
|
751
|
+
const sendRegistry = (instance, id) => {
|
|
752
|
+
const registryEvent = {
|
|
753
|
+
type: "registry",
|
|
754
|
+
data: {
|
|
755
|
+
specId: id,
|
|
756
|
+
endpoints: Array.from(instance.server.registry.endpoints.entries()).map(([key, entry]) => ({
|
|
757
|
+
...entry,
|
|
758
|
+
key
|
|
759
|
+
})),
|
|
760
|
+
stats: { ...instance.server.registry.stats }
|
|
384
761
|
}
|
|
762
|
+
};
|
|
763
|
+
hub.sendTo(client, registryEvent);
|
|
764
|
+
};
|
|
765
|
+
const specId = cmd.data?.specId;
|
|
766
|
+
if (specId) {
|
|
767
|
+
const instance = resolveInstance(specId, instanceMap, hub, client, cmd.type);
|
|
768
|
+
if (!instance) return;
|
|
769
|
+
sendRegistry(instance, specId);
|
|
770
|
+
} else {
|
|
771
|
+
for (const instance of instances) {
|
|
772
|
+
sendRegistry(instance, instance.id);
|
|
385
773
|
}
|
|
386
|
-
return validSeeds;
|
|
387
|
-
} catch (error) {
|
|
388
|
-
logger.error(
|
|
389
|
-
`[vite-plugin-open-api-server] Failed to load seed file ${filePath}:`,
|
|
390
|
-
error instanceof Error ? error.message : error
|
|
391
|
-
);
|
|
392
|
-
return {};
|
|
393
774
|
}
|
|
394
775
|
}
|
|
395
|
-
|
|
396
|
-
const
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
-
|
|
776
|
+
function handleGetTimeline(cmd, hub, client, instances, instanceMap) {
|
|
777
|
+
const specId = cmd.data?.specId;
|
|
778
|
+
const rawLimit = Number(cmd.data?.limit);
|
|
779
|
+
const limit = Number.isFinite(rawLimit) ? Math.min(Math.max(Math.floor(rawLimit), 0), 1e3) : 100;
|
|
780
|
+
const sendTimeline = (instance, id) => {
|
|
781
|
+
const timeline = instance.server.getTimeline();
|
|
782
|
+
const entries = limit === 0 ? [] : timeline.slice(-limit);
|
|
783
|
+
const timelineEvent = {
|
|
784
|
+
type: "timeline",
|
|
785
|
+
data: {
|
|
786
|
+
specId: id,
|
|
787
|
+
entries,
|
|
788
|
+
count: entries.length,
|
|
789
|
+
total: timeline.length
|
|
790
|
+
}
|
|
791
|
+
};
|
|
792
|
+
hub.sendTo(client, timelineEvent);
|
|
793
|
+
};
|
|
794
|
+
if (specId) {
|
|
795
|
+
const instance = resolveInstance(specId, instanceMap, hub, client, cmd.type);
|
|
796
|
+
if (!instance) return;
|
|
797
|
+
sendTimeline(instance, specId);
|
|
798
|
+
} else {
|
|
799
|
+
for (const instance of instances) {
|
|
800
|
+
sendTimeline(instance, instance.id);
|
|
801
|
+
}
|
|
400
802
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
803
|
+
}
|
|
804
|
+
function handleClearTimeline(cmd, hub, client, instances, instanceMap) {
|
|
805
|
+
const specId = cmd.data?.specId;
|
|
806
|
+
const clearAndNotify = (instance, id) => {
|
|
807
|
+
const count = instance.server.clearTimeline();
|
|
808
|
+
hub.sendTo(client, { type: "timeline:cleared", data: { specId: id, count } });
|
|
809
|
+
};
|
|
810
|
+
if (specId) {
|
|
811
|
+
const instance = resolveInstance(specId, instanceMap, hub, client, cmd.type);
|
|
812
|
+
if (!instance) return;
|
|
813
|
+
clearAndNotify(instance, specId);
|
|
814
|
+
} else {
|
|
815
|
+
for (const instance of instances) {
|
|
816
|
+
clearAndNotify(instance, instance.id);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
function handleSpecScoped(cmd, hub, client, instanceMap) {
|
|
821
|
+
const specId = cmd.data?.specId;
|
|
822
|
+
if (!specId) {
|
|
823
|
+
sendError(hub, client, cmd.type, "specId is required for this command");
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
const instance = resolveInstance(specId, instanceMap, hub, client, cmd.type);
|
|
827
|
+
if (!instance) return;
|
|
828
|
+
const { specId: _, ...coreData } = cmd.data ?? {};
|
|
829
|
+
const coreCommand = Object.keys(coreData).length > 0 ? { type: cmd.type, data: coreData } : { type: cmd.type };
|
|
830
|
+
instance.server.wsHub.handleMessage(client, JSON.stringify(coreCommand));
|
|
831
|
+
}
|
|
832
|
+
function createMultiSpecCommandHandler(deps) {
|
|
833
|
+
const { hub, instances, specsInfo, serverVersion } = deps;
|
|
834
|
+
const instanceMap = new Map(instances.map((i) => [i.id, i]));
|
|
835
|
+
return (client, command) => {
|
|
836
|
+
if (!command || typeof command !== "object" || !("type" in command)) {
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
const cmd = command;
|
|
840
|
+
if (typeof cmd.type !== "string") {
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
switch (cmd.type) {
|
|
844
|
+
case "get:specs":
|
|
845
|
+
handleGetSpecs(hub, client, specsInfo, serverVersion);
|
|
846
|
+
break;
|
|
847
|
+
case "get:registry":
|
|
848
|
+
handleGetRegistry(cmd, hub, client, instances, instanceMap);
|
|
849
|
+
break;
|
|
850
|
+
case "get:timeline":
|
|
851
|
+
handleGetTimeline(cmd, hub, client, instances, instanceMap);
|
|
852
|
+
break;
|
|
853
|
+
case "clear:timeline":
|
|
854
|
+
handleClearTimeline(cmd, hub, client, instances, instanceMap);
|
|
855
|
+
break;
|
|
856
|
+
default:
|
|
857
|
+
if (SPEC_SCOPED_COMMANDS.has(cmd.type)) {
|
|
858
|
+
handleSpecScoped(cmd, hub, client, instanceMap);
|
|
859
|
+
} else {
|
|
860
|
+
sendError(hub, client, cmd.type, `Unknown command type: ${cmd.type}`);
|
|
861
|
+
}
|
|
862
|
+
break;
|
|
863
|
+
}
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// src/multi-ws.ts
|
|
868
|
+
var PACKAGE_VERSION2 = package_default.version;
|
|
869
|
+
var MULTI_SPEC_ONLY_COMMANDS = /* @__PURE__ */ new Set(["get:specs"]);
|
|
870
|
+
function createMultiSpecWebSocketHub(instances, specsInfo) {
|
|
871
|
+
const hub = createWebSocketHub({ autoConnect: false });
|
|
872
|
+
const originalAddClient = hub.addClient.bind(hub);
|
|
873
|
+
hub.addClient = (ws) => {
|
|
874
|
+
originalAddClient(ws);
|
|
875
|
+
hub.sendTo(ws, {
|
|
876
|
+
type: "connected",
|
|
877
|
+
// biome-ignore lint/suspicious/noExplicitAny: MultiSpecServerEvent extends ServerEvent with specs[]
|
|
878
|
+
data: { serverVersion: PACKAGE_VERSION2, specs: specsInfo }
|
|
879
|
+
});
|
|
880
|
+
};
|
|
881
|
+
for (const instance of instances) {
|
|
882
|
+
instance.server.wsHub.broadcast = (event) => {
|
|
883
|
+
const enriched = { type: event.type, data: { ...event.data, specId: instance.id } };
|
|
884
|
+
hub.broadcast(enriched);
|
|
885
|
+
};
|
|
886
|
+
instance.server.wsHub.sendTo = (client, event) => {
|
|
887
|
+
const enriched = { type: event.type, data: { ...event.data, specId: instance.id } };
|
|
888
|
+
return hub.sendTo(client, enriched);
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
const commandHandler = createMultiSpecCommandHandler({
|
|
892
|
+
hub,
|
|
893
|
+
instances,
|
|
894
|
+
specsInfo,
|
|
895
|
+
serverVersion: PACKAGE_VERSION2
|
|
407
896
|
});
|
|
408
|
-
|
|
897
|
+
hub.setCommandHandler(commandHandler);
|
|
898
|
+
const originalHandleMessage = hub.handleMessage.bind(hub);
|
|
899
|
+
hub.handleMessage = (client, message) => {
|
|
900
|
+
try {
|
|
901
|
+
const parsed = typeof message === "string" ? JSON.parse(message) : message;
|
|
902
|
+
if (parsed && typeof parsed === "object" && "type" in parsed) {
|
|
903
|
+
const cmd = parsed;
|
|
904
|
+
if (MULTI_SPEC_ONLY_COMMANDS.has(cmd.type)) {
|
|
905
|
+
commandHandler(client, cmd);
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
} catch {
|
|
910
|
+
}
|
|
911
|
+
originalHandleMessage(client, message);
|
|
912
|
+
};
|
|
913
|
+
return hub;
|
|
409
914
|
}
|
|
410
915
|
|
|
411
916
|
// src/types.ts
|
|
@@ -426,6 +931,12 @@ function validateSpecs(specs) {
|
|
|
426
931
|
}
|
|
427
932
|
for (let i = 0; i < specs.length; i++) {
|
|
428
933
|
const spec = specs[i];
|
|
934
|
+
if (!spec || typeof spec !== "object") {
|
|
935
|
+
throw new ValidationError(
|
|
936
|
+
"SPEC_NOT_FOUND",
|
|
937
|
+
`specs[${i}]: must be a SpecConfig object, got ${spec === null ? "null" : typeof spec}`
|
|
938
|
+
);
|
|
939
|
+
}
|
|
429
940
|
if (!spec.spec || typeof spec.spec !== "string" || spec.spec.trim() === "") {
|
|
430
941
|
const identifier = spec.id ? ` (id: "${spec.id}")` : "";
|
|
431
942
|
throw new ValidationError(
|
|
@@ -440,9 +951,12 @@ function resolveOptions(options) {
|
|
|
440
951
|
return {
|
|
441
952
|
specs: options.specs.map((s) => ({
|
|
442
953
|
spec: s.spec,
|
|
954
|
+
// Placeholder — populated by orchestrator after document processing
|
|
443
955
|
id: s.id ?? "",
|
|
956
|
+
// Placeholder — populated by orchestrator after document processing
|
|
444
957
|
proxyPath: s.proxyPath ?? "",
|
|
445
|
-
|
|
958
|
+
// Preliminary — overwritten by deriveProxyPath() during orchestration
|
|
959
|
+
proxyPathSource: s.proxyPath?.trim() ? "explicit" : "auto",
|
|
446
960
|
handlersDir: s.handlersDir ?? "",
|
|
447
961
|
seedsDir: s.seedsDir ?? "",
|
|
448
962
|
idFields: s.idFields ?? {}
|
|
@@ -458,18 +972,468 @@ function resolveOptions(options) {
|
|
|
458
972
|
};
|
|
459
973
|
}
|
|
460
974
|
|
|
975
|
+
// src/proxy-path.ts
|
|
976
|
+
var RESERVED_PROXY_PATHS = [
|
|
977
|
+
DEVTOOLS_PROXY_PATH,
|
|
978
|
+
API_PROXY_PATH,
|
|
979
|
+
WS_PROXY_PATH
|
|
980
|
+
];
|
|
981
|
+
function deriveProxyPath(explicitPath, document, specId) {
|
|
982
|
+
if (explicitPath.trim()) {
|
|
983
|
+
return {
|
|
984
|
+
proxyPath: normalizeProxyPath(explicitPath.trim(), specId),
|
|
985
|
+
proxyPathSource: "explicit"
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
const servers = document.servers;
|
|
989
|
+
const serverUrl = servers?.[0]?.url?.trim();
|
|
990
|
+
if (!serverUrl) {
|
|
991
|
+
throw new ValidationError(
|
|
992
|
+
"PROXY_PATH_MISSING",
|
|
993
|
+
`[${specId}] Cannot derive proxyPath: no servers defined in the OpenAPI document. Set an explicit proxyPath in the spec configuration.`
|
|
994
|
+
);
|
|
995
|
+
}
|
|
996
|
+
let path4;
|
|
997
|
+
let parsedUrl;
|
|
998
|
+
try {
|
|
999
|
+
parsedUrl = new URL(serverUrl);
|
|
1000
|
+
} catch {
|
|
1001
|
+
}
|
|
1002
|
+
if (parsedUrl) {
|
|
1003
|
+
try {
|
|
1004
|
+
path4 = decodeURIComponent(parsedUrl.pathname);
|
|
1005
|
+
} catch {
|
|
1006
|
+
path4 = parsedUrl.pathname;
|
|
1007
|
+
}
|
|
1008
|
+
} else {
|
|
1009
|
+
path4 = serverUrl;
|
|
1010
|
+
}
|
|
1011
|
+
return {
|
|
1012
|
+
proxyPath: normalizeProxyPath(path4, specId),
|
|
1013
|
+
proxyPathSource: "auto"
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
function normalizeProxyPath(path4, specId) {
|
|
1017
|
+
path4 = path4.trim();
|
|
1018
|
+
const queryIdx = path4.indexOf("?");
|
|
1019
|
+
const hashIdx = path4.indexOf("#");
|
|
1020
|
+
const cutIdx = Math.min(
|
|
1021
|
+
queryIdx >= 0 ? queryIdx : path4.length,
|
|
1022
|
+
hashIdx >= 0 ? hashIdx : path4.length
|
|
1023
|
+
);
|
|
1024
|
+
let normalized = path4.slice(0, cutIdx);
|
|
1025
|
+
normalized = normalized.startsWith("/") ? normalized : `/${normalized}`;
|
|
1026
|
+
normalized = normalized.replace(/\/{2,}/g, "/");
|
|
1027
|
+
const segments = normalized.split("/");
|
|
1028
|
+
const resolved = [];
|
|
1029
|
+
for (const segment of segments) {
|
|
1030
|
+
if (segment === ".") {
|
|
1031
|
+
continue;
|
|
1032
|
+
}
|
|
1033
|
+
if (segment === "..") {
|
|
1034
|
+
if (resolved.length > 1) {
|
|
1035
|
+
resolved.pop();
|
|
1036
|
+
}
|
|
1037
|
+
continue;
|
|
1038
|
+
}
|
|
1039
|
+
resolved.push(segment);
|
|
1040
|
+
}
|
|
1041
|
+
normalized = resolved.join("/") || "/";
|
|
1042
|
+
if (normalized.length > 1 && normalized.endsWith("/")) {
|
|
1043
|
+
normalized = normalized.slice(0, -1);
|
|
1044
|
+
}
|
|
1045
|
+
if (normalized === "/") {
|
|
1046
|
+
throw new ValidationError(
|
|
1047
|
+
"PROXY_PATH_TOO_BROAD",
|
|
1048
|
+
`[${specId}] proxyPath "/" is too broad \u2014 it would capture all requests. Set a more specific proxyPath (e.g., "/api/v1").`
|
|
1049
|
+
);
|
|
1050
|
+
}
|
|
1051
|
+
return normalized;
|
|
1052
|
+
}
|
|
1053
|
+
function validateUniqueProxyPaths(specs) {
|
|
1054
|
+
const paths = /* @__PURE__ */ new Map();
|
|
1055
|
+
for (const spec of specs) {
|
|
1056
|
+
const path4 = spec.proxyPath?.trim();
|
|
1057
|
+
if (!path4) {
|
|
1058
|
+
continue;
|
|
1059
|
+
}
|
|
1060
|
+
validateNotReservedPath(path4, spec.id);
|
|
1061
|
+
if (paths.has(path4)) {
|
|
1062
|
+
throw new ValidationError(
|
|
1063
|
+
"PROXY_PATH_DUPLICATE",
|
|
1064
|
+
`Duplicate proxyPath "${path4}" used by specs "${paths.get(path4)}" and "${spec.id}". Each spec must have a unique proxyPath.`
|
|
1065
|
+
);
|
|
1066
|
+
}
|
|
1067
|
+
paths.set(path4, spec.id);
|
|
1068
|
+
}
|
|
1069
|
+
validateNoPrefixOverlaps(paths);
|
|
1070
|
+
}
|
|
1071
|
+
function validateNotReservedPath(path4, specId) {
|
|
1072
|
+
for (const reserved of RESERVED_PROXY_PATHS) {
|
|
1073
|
+
if (path4 === reserved || path4.startsWith(`${reserved}/`) || reserved.startsWith(path4)) {
|
|
1074
|
+
throw new ValidationError(
|
|
1075
|
+
"PROXY_PATH_OVERLAP",
|
|
1076
|
+
`[${specId}] proxyPath "${path4}" collides with reserved path "${reserved}" used by the shared DevTools/API/WebSocket service.`
|
|
1077
|
+
);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
function validateNoPrefixOverlaps(paths) {
|
|
1082
|
+
const sortedPaths = Array.from(paths.entries()).sort(([a], [b]) => a.length - b.length);
|
|
1083
|
+
for (let i = 0; i < sortedPaths.length; i++) {
|
|
1084
|
+
for (let j = i + 1; j < sortedPaths.length; j++) {
|
|
1085
|
+
const [shorter, shorterId] = sortedPaths[i];
|
|
1086
|
+
const [longer, longerId] = sortedPaths[j];
|
|
1087
|
+
if (longer.startsWith(`${shorter}/`)) {
|
|
1088
|
+
throw new ValidationError(
|
|
1089
|
+
"PROXY_PATH_OVERLAP",
|
|
1090
|
+
`Overlapping proxyPaths: "${shorter}" (${shorterId}) is a prefix of "${longer}" (${longerId}). This would cause routing ambiguity.`
|
|
1091
|
+
);
|
|
1092
|
+
}
|
|
1093
|
+
if (longer.startsWith(shorter)) {
|
|
1094
|
+
throw new ValidationError(
|
|
1095
|
+
"PROXY_PATH_PREFIX_COLLISION",
|
|
1096
|
+
`Proxy path prefix collision: "${shorter}" (${shorterId}) is a string prefix of "${longer}" (${longerId}). Vite's proxy uses startsWith matching, so "${shorter}" would incorrectly capture requests meant for "${longer}".`
|
|
1097
|
+
);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
// src/spec-id.ts
|
|
1104
|
+
function slugify(input) {
|
|
1105
|
+
return input.normalize("NFD").replace(new RegExp("\\p{M}", "gu"), "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
1106
|
+
}
|
|
1107
|
+
function deriveSpecId(explicitId, document) {
|
|
1108
|
+
if (explicitId.trim()) {
|
|
1109
|
+
const id2 = slugify(explicitId);
|
|
1110
|
+
if (!id2) {
|
|
1111
|
+
throw new ValidationError(
|
|
1112
|
+
"SPEC_ID_MISSING",
|
|
1113
|
+
`Cannot derive spec ID: explicit id "${explicitId}" produces an empty slug. Please provide an id containing ASCII letters or numbers.`
|
|
1114
|
+
);
|
|
1115
|
+
}
|
|
1116
|
+
return id2;
|
|
1117
|
+
}
|
|
1118
|
+
const title = document.info?.title;
|
|
1119
|
+
if (!title || !title.trim()) {
|
|
1120
|
+
throw new ValidationError(
|
|
1121
|
+
"SPEC_ID_MISSING",
|
|
1122
|
+
"Cannot derive spec ID: info.title is missing from the OpenAPI document. Please set an explicit id in the spec configuration."
|
|
1123
|
+
);
|
|
1124
|
+
}
|
|
1125
|
+
const id = slugify(title);
|
|
1126
|
+
if (!id) {
|
|
1127
|
+
throw new ValidationError(
|
|
1128
|
+
"SPEC_ID_MISSING",
|
|
1129
|
+
`Cannot derive spec ID: info.title "${title}" produces an empty slug. Please set an explicit id in the spec configuration.`
|
|
1130
|
+
);
|
|
1131
|
+
}
|
|
1132
|
+
return id;
|
|
1133
|
+
}
|
|
1134
|
+
function validateUniqueIds(ids) {
|
|
1135
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1136
|
+
const duplicates = /* @__PURE__ */ new Set();
|
|
1137
|
+
for (const id of ids) {
|
|
1138
|
+
if (seen.has(id)) {
|
|
1139
|
+
duplicates.add(id);
|
|
1140
|
+
}
|
|
1141
|
+
seen.add(id);
|
|
1142
|
+
}
|
|
1143
|
+
if (duplicates.size > 0) {
|
|
1144
|
+
const list = [...duplicates].join(", ");
|
|
1145
|
+
throw new ValidationError(
|
|
1146
|
+
"SPEC_ID_DUPLICATE",
|
|
1147
|
+
`Duplicate spec IDs: ${list}. Each spec must have a unique ID. Set explicit ids in spec configuration to resolve.`
|
|
1148
|
+
);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
// src/orchestrator.ts
|
|
1153
|
+
var SPEC_COLORS = [
|
|
1154
|
+
"#4ade80",
|
|
1155
|
+
// green
|
|
1156
|
+
"#60a5fa",
|
|
1157
|
+
// blue
|
|
1158
|
+
"#f472b6",
|
|
1159
|
+
// pink
|
|
1160
|
+
"#facc15",
|
|
1161
|
+
// yellow
|
|
1162
|
+
"#a78bfa",
|
|
1163
|
+
// purple
|
|
1164
|
+
"#fb923c",
|
|
1165
|
+
// orange
|
|
1166
|
+
"#2dd4bf",
|
|
1167
|
+
// teal
|
|
1168
|
+
"#f87171"
|
|
1169
|
+
// red
|
|
1170
|
+
];
|
|
1171
|
+
async function processSpec(specConfig, index, options, vite, cwd, logger) {
|
|
1172
|
+
const server = await createOpenApiServer({
|
|
1173
|
+
spec: specConfig.spec,
|
|
1174
|
+
port: options.port,
|
|
1175
|
+
idFields: specConfig.idFields,
|
|
1176
|
+
handlers: /* @__PURE__ */ new Map(),
|
|
1177
|
+
seeds: /* @__PURE__ */ new Map(),
|
|
1178
|
+
timelineLimit: options.timelineLimit,
|
|
1179
|
+
cors: false,
|
|
1180
|
+
// CORS handled at main app level
|
|
1181
|
+
devtools: false,
|
|
1182
|
+
// DevTools mounted at main app level
|
|
1183
|
+
logger
|
|
1184
|
+
});
|
|
1185
|
+
const id = deriveSpecId(specConfig.id, server.document);
|
|
1186
|
+
const handlersDir = specConfig.handlersDir || `./mocks/${id}/handlers`;
|
|
1187
|
+
const seedsDir = specConfig.seedsDir || `./mocks/${id}/seeds`;
|
|
1188
|
+
const handlersResult = await loadHandlers(handlersDir, vite, cwd, logger);
|
|
1189
|
+
const seedsResult = await loadSeeds(seedsDir, vite, cwd, logger);
|
|
1190
|
+
if (handlersResult.handlers.size > 0) {
|
|
1191
|
+
server.updateHandlers(handlersResult.handlers, { silent: true });
|
|
1192
|
+
}
|
|
1193
|
+
if (seedsResult.seeds.size > 0) {
|
|
1194
|
+
await executeSeeds(seedsResult.seeds, server.store, server.document);
|
|
1195
|
+
}
|
|
1196
|
+
const { proxyPath, proxyPathSource } = deriveProxyPath(specConfig.proxyPath, server.document, id);
|
|
1197
|
+
const info = {
|
|
1198
|
+
id,
|
|
1199
|
+
title: server.document.info?.title ?? id,
|
|
1200
|
+
version: server.document.info?.version ?? "unknown",
|
|
1201
|
+
proxyPath,
|
|
1202
|
+
color: SPEC_COLORS[index % SPEC_COLORS.length],
|
|
1203
|
+
endpointCount: server.registry.endpoints.size,
|
|
1204
|
+
schemaCount: server.store.getSchemas().length
|
|
1205
|
+
};
|
|
1206
|
+
return {
|
|
1207
|
+
instance: { id, info, server, config: specConfig },
|
|
1208
|
+
resolvedConfig: { id, proxyPath, proxyPathSource, handlersDir, seedsDir }
|
|
1209
|
+
};
|
|
1210
|
+
}
|
|
1211
|
+
function buildCorsConfig(options) {
|
|
1212
|
+
const isWildcardOrigin = options.corsOrigin === "*" || Array.isArray(options.corsOrigin) && options.corsOrigin.includes("*");
|
|
1213
|
+
const effectiveCorsOrigin = isWildcardOrigin ? "*" : options.corsOrigin;
|
|
1214
|
+
return {
|
|
1215
|
+
origin: effectiveCorsOrigin,
|
|
1216
|
+
allowMethods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"],
|
|
1217
|
+
allowHeaders: ["Content-Type", "Authorization", "X-Requested-With", "X-Spec-Id"],
|
|
1218
|
+
exposeHeaders: ["Content-Length", "X-Request-Id"],
|
|
1219
|
+
maxAge: 86400,
|
|
1220
|
+
credentials: !isWildcardOrigin
|
|
1221
|
+
};
|
|
1222
|
+
}
|
|
1223
|
+
function mountDevToolsSpa(mainApp, logger) {
|
|
1224
|
+
const pluginDir = dirname(fileURLToPath(import.meta.url));
|
|
1225
|
+
const spaDir = join(pluginDir, "devtools-spa");
|
|
1226
|
+
const devtoolsSpaDir = existsSync(spaDir) ? spaDir : void 0;
|
|
1227
|
+
if (!devtoolsSpaDir) {
|
|
1228
|
+
logger.warn?.(
|
|
1229
|
+
"[vite-plugin-open-api-server] DevTools SPA not found at",
|
|
1230
|
+
spaDir,
|
|
1231
|
+
'- serving placeholder. Run "pnpm build" to include the SPA.'
|
|
1232
|
+
);
|
|
1233
|
+
}
|
|
1234
|
+
mountDevToolsRoutes(mainApp, {
|
|
1235
|
+
spaDir: devtoolsSpaDir,
|
|
1236
|
+
logger
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1239
|
+
async function mountWebSocketRoute(mainApp, wsHub, logger) {
|
|
1240
|
+
let nodeWsModule;
|
|
1241
|
+
try {
|
|
1242
|
+
nodeWsModule = await import('@hono/node-ws');
|
|
1243
|
+
} catch (err) {
|
|
1244
|
+
const isModuleNotFound = err instanceof Error && ("code" in err ? err.code === "ERR_MODULE_NOT_FOUND" : err.message.includes("@hono/node-ws"));
|
|
1245
|
+
if (!isModuleNotFound) {
|
|
1246
|
+
throw err;
|
|
1247
|
+
}
|
|
1248
|
+
mainApp.get("/_ws", (c) => {
|
|
1249
|
+
return c.json(
|
|
1250
|
+
{
|
|
1251
|
+
message: "WebSocket endpoint - use ws:// protocol",
|
|
1252
|
+
note: "Install @hono/node-ws to enable WebSocket support"
|
|
1253
|
+
},
|
|
1254
|
+
501
|
|
1255
|
+
);
|
|
1256
|
+
});
|
|
1257
|
+
logger.debug?.(
|
|
1258
|
+
"[vite-plugin-open-api-server] @hono/node-ws not available, WebSocket upgrade disabled"
|
|
1259
|
+
);
|
|
1260
|
+
return { injectWebSocket: null };
|
|
1261
|
+
}
|
|
1262
|
+
const nodeWs = nodeWsModule.createNodeWebSocket({ app: mainApp });
|
|
1263
|
+
mainApp.get(
|
|
1264
|
+
"/_ws",
|
|
1265
|
+
nodeWs.upgradeWebSocket(() => ({
|
|
1266
|
+
onOpen(_event, ws) {
|
|
1267
|
+
wsHub.addClient(ws);
|
|
1268
|
+
},
|
|
1269
|
+
onMessage(event, ws) {
|
|
1270
|
+
wsHub.handleMessage(ws, event.data);
|
|
1271
|
+
},
|
|
1272
|
+
onClose(_event, ws) {
|
|
1273
|
+
wsHub.removeClient(ws);
|
|
1274
|
+
}
|
|
1275
|
+
}))
|
|
1276
|
+
);
|
|
1277
|
+
logger.debug?.("[vite-plugin-open-api-server] WebSocket upgrade enabled at /_ws");
|
|
1278
|
+
return { injectWebSocket: nodeWs.injectWebSocket };
|
|
1279
|
+
}
|
|
1280
|
+
function createDispatchMiddleware(instanceMap) {
|
|
1281
|
+
return async (c, next) => {
|
|
1282
|
+
const rawSpecId = c.req.header("x-spec-id");
|
|
1283
|
+
if (!rawSpecId) {
|
|
1284
|
+
await next();
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
const specId = slugify(rawSpecId.trim());
|
|
1288
|
+
if (!specId) {
|
|
1289
|
+
await next();
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
1292
|
+
const instance = instanceMap.get(specId);
|
|
1293
|
+
if (!instance) {
|
|
1294
|
+
return c.json({ error: `Unknown spec: ${specId}` }, 404);
|
|
1295
|
+
}
|
|
1296
|
+
return instance.server.app.fetch(c.req.raw);
|
|
1297
|
+
};
|
|
1298
|
+
}
|
|
1299
|
+
async function createOrchestrator(options, vite, cwd) {
|
|
1300
|
+
const logger = options.logger ?? console;
|
|
1301
|
+
const instances = [];
|
|
1302
|
+
for (let i = 0; i < options.specs.length; i++) {
|
|
1303
|
+
const { instance, resolvedConfig } = await processSpec(
|
|
1304
|
+
options.specs[i],
|
|
1305
|
+
i,
|
|
1306
|
+
options,
|
|
1307
|
+
vite,
|
|
1308
|
+
cwd,
|
|
1309
|
+
logger
|
|
1310
|
+
);
|
|
1311
|
+
instance.config.id = resolvedConfig.id;
|
|
1312
|
+
instance.config.proxyPath = resolvedConfig.proxyPath;
|
|
1313
|
+
instance.config.proxyPathSource = resolvedConfig.proxyPathSource;
|
|
1314
|
+
instance.config.handlersDir = resolvedConfig.handlersDir;
|
|
1315
|
+
instance.config.seedsDir = resolvedConfig.seedsDir;
|
|
1316
|
+
instances.push(instance);
|
|
1317
|
+
}
|
|
1318
|
+
validateUniqueIds(instances.map((inst) => inst.id));
|
|
1319
|
+
validateUniqueProxyPaths(
|
|
1320
|
+
instances.map((inst) => ({
|
|
1321
|
+
id: inst.id,
|
|
1322
|
+
proxyPath: inst.config.proxyPath
|
|
1323
|
+
}))
|
|
1324
|
+
);
|
|
1325
|
+
const mainApp = new Hono();
|
|
1326
|
+
const corsConfig = buildCorsConfig(options);
|
|
1327
|
+
if (options.cors) {
|
|
1328
|
+
mainApp.use("*", cors(corsConfig));
|
|
1329
|
+
for (const inst of instances) {
|
|
1330
|
+
inst.server.app.use("*", cors(corsConfig));
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
if (options.devtools) {
|
|
1334
|
+
mountDevToolsSpa(mainApp, logger);
|
|
1335
|
+
}
|
|
1336
|
+
if (instances.length > 0) {
|
|
1337
|
+
mountMultiSpecInternalApi(mainApp, instances);
|
|
1338
|
+
}
|
|
1339
|
+
const specsInfo = instances.map((inst) => inst.info);
|
|
1340
|
+
const wsHub = createMultiSpecWebSocketHub(instances, specsInfo);
|
|
1341
|
+
const { injectWebSocket } = await mountWebSocketRoute(mainApp, wsHub, logger);
|
|
1342
|
+
const instanceMap = new Map(instances.map((inst) => [inst.id, inst]));
|
|
1343
|
+
mainApp.use("*", createDispatchMiddleware(instanceMap));
|
|
1344
|
+
let serverInstance = null;
|
|
1345
|
+
let boundPort = 0;
|
|
1346
|
+
async function startServerOnPort(fetchHandler, port) {
|
|
1347
|
+
let createAdaptorServer;
|
|
1348
|
+
try {
|
|
1349
|
+
const nodeServer = await import('@hono/node-server');
|
|
1350
|
+
createAdaptorServer = nodeServer.createAdaptorServer;
|
|
1351
|
+
} catch {
|
|
1352
|
+
throw new Error("@hono/node-server is required. Install with: npm install @hono/node-server");
|
|
1353
|
+
}
|
|
1354
|
+
const server = createAdaptorServer({ fetch: fetchHandler });
|
|
1355
|
+
const actualPort = await new Promise((resolve, reject) => {
|
|
1356
|
+
const onListening = () => {
|
|
1357
|
+
server.removeListener("error", onError);
|
|
1358
|
+
const addr = server.address();
|
|
1359
|
+
resolve(typeof addr === "object" && addr ? addr.port : port);
|
|
1360
|
+
};
|
|
1361
|
+
const onError = (err) => {
|
|
1362
|
+
server.removeListener("listening", onListening);
|
|
1363
|
+
server.close(() => {
|
|
1364
|
+
});
|
|
1365
|
+
if (err.code === "EADDRINUSE") {
|
|
1366
|
+
reject(new Error(`[vite-plugin-open-api-server] Port ${port} is already in use.`));
|
|
1367
|
+
} else {
|
|
1368
|
+
reject(new Error(`[vite-plugin-open-api-server] Server error: ${err.message}`));
|
|
1369
|
+
}
|
|
1370
|
+
};
|
|
1371
|
+
server.once("listening", onListening);
|
|
1372
|
+
server.once("error", onError);
|
|
1373
|
+
server.listen(port);
|
|
1374
|
+
});
|
|
1375
|
+
return { server, actualPort };
|
|
1376
|
+
}
|
|
1377
|
+
return {
|
|
1378
|
+
app: mainApp,
|
|
1379
|
+
instances,
|
|
1380
|
+
specsInfo,
|
|
1381
|
+
wsHub,
|
|
1382
|
+
get port() {
|
|
1383
|
+
return boundPort;
|
|
1384
|
+
},
|
|
1385
|
+
async start() {
|
|
1386
|
+
if (serverInstance) {
|
|
1387
|
+
throw new Error("[vite-plugin-open-api-server] Server already running. Call stop() first.");
|
|
1388
|
+
}
|
|
1389
|
+
const { server, actualPort } = await startServerOnPort(mainApp.fetch, options.port);
|
|
1390
|
+
serverInstance = server;
|
|
1391
|
+
boundPort = actualPort;
|
|
1392
|
+
if (injectWebSocket) {
|
|
1393
|
+
injectWebSocket(serverInstance);
|
|
1394
|
+
}
|
|
1395
|
+
logger.info(`[vite-plugin-open-api-server] Server started on http://localhost:${actualPort}`);
|
|
1396
|
+
},
|
|
1397
|
+
async stop() {
|
|
1398
|
+
const server = serverInstance;
|
|
1399
|
+
if (server) {
|
|
1400
|
+
try {
|
|
1401
|
+
wsHub.clear();
|
|
1402
|
+
if (typeof server.closeAllConnections === "function") {
|
|
1403
|
+
server.closeAllConnections();
|
|
1404
|
+
}
|
|
1405
|
+
await new Promise((resolve, reject) => {
|
|
1406
|
+
server.close((err) => {
|
|
1407
|
+
if (err) {
|
|
1408
|
+
reject(err);
|
|
1409
|
+
} else {
|
|
1410
|
+
resolve();
|
|
1411
|
+
}
|
|
1412
|
+
});
|
|
1413
|
+
});
|
|
1414
|
+
logger.info("[vite-plugin-open-api-server] Server stopped");
|
|
1415
|
+
} catch (err) {
|
|
1416
|
+
logger.error?.("[vite-plugin-open-api-server] Error closing server:", err);
|
|
1417
|
+
throw err;
|
|
1418
|
+
} finally {
|
|
1419
|
+
serverInstance = null;
|
|
1420
|
+
boundPort = 0;
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
};
|
|
1425
|
+
}
|
|
1426
|
+
|
|
461
1427
|
// src/plugin.ts
|
|
462
1428
|
var VIRTUAL_DEVTOOLS_TAB_ID = "virtual:open-api-devtools-tab";
|
|
463
1429
|
var RESOLVED_VIRTUAL_DEVTOOLS_TAB_ID = `\0${VIRTUAL_DEVTOOLS_TAB_ID}`;
|
|
464
1430
|
function openApiServer(options) {
|
|
465
1431
|
const resolvedOptions = resolveOptions(options);
|
|
466
|
-
let
|
|
467
|
-
let
|
|
468
|
-
let fileWatcher = null;
|
|
1432
|
+
let orchestrator = null;
|
|
1433
|
+
let fileWatchers = [];
|
|
469
1434
|
let cwd = process.cwd();
|
|
470
1435
|
return {
|
|
471
1436
|
name: "vite-plugin-open-api-server",
|
|
472
|
-
// Only active in dev mode
|
|
473
1437
|
apply: "serve",
|
|
474
1438
|
/**
|
|
475
1439
|
* Ensure @vue/devtools-api is available for the DevTools tab module
|
|
@@ -515,7 +1479,7 @@ import { addCustomTab } from '@vue/devtools-api';
|
|
|
515
1479
|
|
|
516
1480
|
try {
|
|
517
1481
|
// Route through Vite's proxy so it works in all environments
|
|
518
|
-
const iframeSrc = window.location.origin + '/
|
|
1482
|
+
const iframeSrc = window.location.origin + '${DEVTOOLS_PROXY_PATH}/';
|
|
519
1483
|
|
|
520
1484
|
addCustomTab({
|
|
521
1485
|
name: 'vite-plugin-open-api-server',
|
|
@@ -538,78 +1502,50 @@ try {
|
|
|
538
1502
|
*
|
|
539
1503
|
* This hook is called when the dev server is created.
|
|
540
1504
|
* We use it to:
|
|
541
|
-
* 1.
|
|
542
|
-
* 2. Configure
|
|
543
|
-
* 3. Set up file
|
|
1505
|
+
* 1. Create and start the multi-spec orchestrator
|
|
1506
|
+
* 2. Configure Vite's multi-proxy for all specs
|
|
1507
|
+
* 3. Set up per-spec file watchers for hot reload
|
|
1508
|
+
* 4. Print startup banner
|
|
544
1509
|
*/
|
|
545
1510
|
async configureServer(viteServer) {
|
|
546
|
-
vite = viteServer;
|
|
547
1511
|
cwd = viteServer.config.root;
|
|
548
1512
|
if (!resolvedOptions.enabled) {
|
|
549
1513
|
return;
|
|
550
1514
|
}
|
|
551
1515
|
try {
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
devtoolsSpaDir = spaDir;
|
|
560
|
-
} else {
|
|
561
|
-
resolvedOptions.logger?.warn?.(
|
|
562
|
-
"[vite-plugin-open-api-server] DevTools SPA not found at",
|
|
563
|
-
spaDir,
|
|
564
|
-
'- serving placeholder. Run "pnpm build" to include the SPA.'
|
|
565
|
-
);
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
const opts = resolvedOptions;
|
|
569
|
-
server = await createOpenApiServer({
|
|
570
|
-
spec: opts.spec,
|
|
571
|
-
port: opts.port,
|
|
572
|
-
idFields: opts.idFields,
|
|
573
|
-
handlers: handlersResult.handlers,
|
|
574
|
-
// Seeds are populated via executeSeeds, not directly
|
|
575
|
-
seeds: /* @__PURE__ */ new Map(),
|
|
576
|
-
timelineLimit: opts.timelineLimit,
|
|
577
|
-
devtools: opts.devtools,
|
|
578
|
-
devtoolsSpaDir,
|
|
579
|
-
cors: opts.cors,
|
|
580
|
-
corsOrigin: opts.corsOrigin,
|
|
581
|
-
logger: opts.logger
|
|
582
|
-
});
|
|
583
|
-
if (seedsResult.seeds.size > 0) {
|
|
584
|
-
await executeSeeds(seedsResult.seeds, server.store, server.document);
|
|
585
|
-
}
|
|
586
|
-
await server.start();
|
|
587
|
-
configureProxy(viteServer, resolvedOptions.proxyPath, resolvedOptions.port);
|
|
588
|
-
const bannerInfo = extractBannerInfo(
|
|
589
|
-
server.registry,
|
|
590
|
-
{
|
|
591
|
-
info: {
|
|
592
|
-
title: server.document.info?.title ?? "OpenAPI Server",
|
|
593
|
-
version: server.document.info?.version ?? "1.0.0"
|
|
594
|
-
}
|
|
595
|
-
},
|
|
596
|
-
handlersResult.handlers.size,
|
|
597
|
-
seedsResult.seeds.size,
|
|
1516
|
+
orchestrator = await createOrchestrator(resolvedOptions, viteServer, cwd);
|
|
1517
|
+
await orchestrator.start();
|
|
1518
|
+
configureMultiProxy(viteServer, orchestrator.instances, orchestrator.port);
|
|
1519
|
+
fileWatchers = await createPerSpecFileWatchers(
|
|
1520
|
+
orchestrator.instances,
|
|
1521
|
+
viteServer,
|
|
1522
|
+
cwd,
|
|
598
1523
|
resolvedOptions
|
|
599
1524
|
);
|
|
600
|
-
|
|
601
|
-
|
|
1525
|
+
if (!resolvedOptions.silent && orchestrator.instances.length > 0) {
|
|
1526
|
+
const firstInstance = orchestrator.instances[0];
|
|
1527
|
+
const bannerInfo = extractBannerInfo(
|
|
1528
|
+
firstInstance.server.registry,
|
|
1529
|
+
{
|
|
1530
|
+
info: {
|
|
1531
|
+
title: firstInstance.server.document.info?.title ?? "OpenAPI Server",
|
|
1532
|
+
version: firstInstance.server.document.info?.version ?? "1.0.0"
|
|
1533
|
+
}
|
|
1534
|
+
},
|
|
1535
|
+
// Handler/seed counts are not tracked per-instance yet.
|
|
1536
|
+
// Multi-spec banner (Epic 5, Task 5.1) will display proper per-spec counts.
|
|
1537
|
+
0,
|
|
1538
|
+
0,
|
|
1539
|
+
resolvedOptions
|
|
1540
|
+
);
|
|
1541
|
+
printBanner(bannerInfo, resolvedOptions);
|
|
1542
|
+
}
|
|
602
1543
|
} catch (error) {
|
|
1544
|
+
await teardown();
|
|
603
1545
|
printError("Failed to start OpenAPI mock server", error, resolvedOptions);
|
|
604
1546
|
throw error;
|
|
605
1547
|
}
|
|
606
1548
|
},
|
|
607
|
-
/**
|
|
608
|
-
* Clean up when Vite server closes
|
|
609
|
-
*/
|
|
610
|
-
async closeBundle() {
|
|
611
|
-
await cleanup();
|
|
612
|
-
},
|
|
613
1549
|
/**
|
|
614
1550
|
* Inject Vue DevTools custom tab registration script
|
|
615
1551
|
*
|
|
@@ -629,102 +1565,30 @@ try {
|
|
|
629
1565
|
injectTo: "head"
|
|
630
1566
|
}
|
|
631
1567
|
];
|
|
1568
|
+
},
|
|
1569
|
+
/**
|
|
1570
|
+
* Clean up when Vite server closes
|
|
1571
|
+
*
|
|
1572
|
+
* NOTE: closeBundle() is called by Vite when the dev server shuts down
|
|
1573
|
+
* (e.g., Ctrl+C). This is the same lifecycle hook used in v0.x.
|
|
1574
|
+
* While configureServer's viteServer.httpServer?.on('close', ...) is
|
|
1575
|
+
* an alternative, closeBundle() is more reliable across Vite versions
|
|
1576
|
+
* and is the established pattern in this codebase.
|
|
1577
|
+
*/
|
|
1578
|
+
async closeBundle() {
|
|
1579
|
+
await teardown();
|
|
632
1580
|
}
|
|
633
1581
|
};
|
|
634
|
-
function
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
changeOrigin: true,
|
|
642
|
-
// Remove the proxy path prefix when forwarding
|
|
643
|
-
rewrite: (path4) => path4.replace(pathPrefixRegex, "")
|
|
644
|
-
};
|
|
645
|
-
proxyConfig["/_devtools"] = {
|
|
646
|
-
target: `http://localhost:${port}`,
|
|
647
|
-
changeOrigin: true
|
|
648
|
-
};
|
|
649
|
-
proxyConfig["/_api"] = {
|
|
650
|
-
target: `http://localhost:${port}`,
|
|
651
|
-
changeOrigin: true
|
|
652
|
-
};
|
|
653
|
-
proxyConfig["/_ws"] = {
|
|
654
|
-
target: `http://localhost:${port}`,
|
|
655
|
-
changeOrigin: true,
|
|
656
|
-
ws: true
|
|
657
|
-
};
|
|
658
|
-
if (vite2.config.server) {
|
|
659
|
-
vite2.config.server.proxy = proxyConfig;
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
async function setupFileWatching() {
|
|
663
|
-
if (!server || !vite) return;
|
|
664
|
-
const debouncedHandlerReload = debounce(reloadHandlers, 100);
|
|
665
|
-
const debouncedSeedReload = debounce(reloadSeeds, 100);
|
|
666
|
-
const watchOpts = resolvedOptions;
|
|
667
|
-
fileWatcher = await createFileWatcher({
|
|
668
|
-
handlersDir: watchOpts.handlersDir,
|
|
669
|
-
seedsDir: watchOpts.seedsDir,
|
|
670
|
-
cwd,
|
|
671
|
-
onHandlerChange: debouncedHandlerReload,
|
|
672
|
-
onSeedChange: debouncedSeedReload
|
|
673
|
-
});
|
|
674
|
-
}
|
|
675
|
-
async function reloadHandlers() {
|
|
676
|
-
if (!server || !vite) return;
|
|
677
|
-
try {
|
|
678
|
-
const handlersResult = await loadHandlers(
|
|
679
|
-
// biome-ignore lint/suspicious/noExplicitAny: v0.x compat — plugin.ts rewritten in Task 1.7
|
|
680
|
-
resolvedOptions.handlersDir,
|
|
681
|
-
vite,
|
|
682
|
-
cwd
|
|
683
|
-
);
|
|
684
|
-
server.updateHandlers(handlersResult.handlers);
|
|
685
|
-
server.wsHub.broadcast({
|
|
686
|
-
type: "handlers:updated",
|
|
687
|
-
data: { count: handlersResult.handlers.size }
|
|
688
|
-
});
|
|
689
|
-
printReloadNotification("handlers", handlersResult.handlers.size, resolvedOptions);
|
|
690
|
-
} catch (error) {
|
|
691
|
-
printError("Failed to reload handlers", error, resolvedOptions);
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
async function reloadSeeds() {
|
|
695
|
-
if (!server || !vite) return;
|
|
696
|
-
try {
|
|
697
|
-
const seedsResult = await loadSeeds(
|
|
698
|
-
// biome-ignore lint/suspicious/noExplicitAny: v0.x compat — plugin.ts rewritten in Task 1.7
|
|
699
|
-
resolvedOptions.seedsDir,
|
|
700
|
-
vite,
|
|
701
|
-
cwd
|
|
702
|
-
);
|
|
703
|
-
if (seedsResult.seeds.size > 0) {
|
|
704
|
-
server.store.clearAll();
|
|
705
|
-
await executeSeeds(seedsResult.seeds, server.store, server.document);
|
|
706
|
-
} else {
|
|
707
|
-
server.store.clearAll();
|
|
1582
|
+
async function teardown() {
|
|
1583
|
+
await Promise.allSettled(fileWatchers.map((w) => w.close()));
|
|
1584
|
+
fileWatchers = [];
|
|
1585
|
+
if (orchestrator) {
|
|
1586
|
+
try {
|
|
1587
|
+
await orchestrator.stop();
|
|
1588
|
+
} catch {
|
|
708
1589
|
}
|
|
709
|
-
|
|
710
|
-
type: "seeds:updated",
|
|
711
|
-
data: { count: seedsResult.seeds.size }
|
|
712
|
-
});
|
|
713
|
-
printReloadNotification("seeds", seedsResult.seeds.size, resolvedOptions);
|
|
714
|
-
} catch (error) {
|
|
715
|
-
printError("Failed to reload seeds", error, resolvedOptions);
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
async function cleanup() {
|
|
719
|
-
if (fileWatcher) {
|
|
720
|
-
await fileWatcher.close();
|
|
721
|
-
fileWatcher = null;
|
|
722
|
-
}
|
|
723
|
-
if (server) {
|
|
724
|
-
await server.stop();
|
|
725
|
-
server = null;
|
|
1590
|
+
orchestrator = null;
|
|
726
1591
|
}
|
|
727
|
-
vite = null;
|
|
728
1592
|
}
|
|
729
1593
|
}
|
|
730
1594
|
|
|
@@ -758,12 +1622,12 @@ async function registerDevTools(app, options = {}) {
|
|
|
758
1622
|
console.warn("[OpenAPI DevTools] Failed to register with Vue DevTools:", error);
|
|
759
1623
|
}
|
|
760
1624
|
}
|
|
761
|
-
function getDevToolsUrl(port =
|
|
1625
|
+
function getDevToolsUrl(port = 4e3, host, protocol) {
|
|
762
1626
|
const actualProtocol = protocol || (typeof window !== "undefined" ? window.location.protocol.replace(":", "") : "http");
|
|
763
1627
|
const actualHost = host || (typeof window !== "undefined" ? window.location.hostname : "localhost");
|
|
764
1628
|
return `${actualProtocol}://${actualHost}:${port}/_devtools/`;
|
|
765
1629
|
}
|
|
766
1630
|
|
|
767
|
-
export { ValidationError, createFileWatcher, debounce, getDevToolsUrl, getHandlerFiles, getSeedFiles, loadHandlers, loadSeeds, openApiServer, registerDevTools };
|
|
1631
|
+
export { API_PROXY_PATH, DEVTOOLS_PROXY_PATH, SPEC_COLORS, ValidationError, WS_PROXY_PATH, configureMultiProxy, createFileWatcher, createOrchestrator, createPerSpecFileWatchers, debounce, deriveProxyPath, deriveSpecId, getDevToolsUrl, getHandlerFiles, getSeedFiles, loadHandlers, loadSeeds, normalizeProxyPath, openApiServer, registerDevTools, reloadSpecHandlers, reloadSpecSeeds, resolveOptions, slugify, validateSpecs, validateUniqueIds, validateUniqueProxyPaths };
|
|
768
1632
|
//# sourceMappingURL=index.js.map
|
|
769
1633
|
//# sourceMappingURL=index.js.map
|