@websublime/vite-plugin-open-api-server 0.24.0-next.3 → 0.24.0-next.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +111 -17
- package/dist/index.js +560 -525
- package/dist/index.js.map +1 -1
- package/package.json +13 -13
package/dist/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { existsSync } from 'fs';
|
|
2
1
|
import { createRequire } from 'module';
|
|
3
|
-
import
|
|
4
|
-
import { fileURLToPath } from 'url';
|
|
5
|
-
import { createOpenApiServer, executeSeeds, mountInternalApi, mountDevToolsRoutes } from '@websublime/vite-plugin-open-api-core';
|
|
2
|
+
import { mountInternalApi, executeSeeds, createOpenApiServer, mountDevToolsRoutes } from '@websublime/vite-plugin-open-api-core';
|
|
6
3
|
export { defineHandlers, defineSeeds } from '@websublime/vite-plugin-open-api-core';
|
|
7
4
|
import pc from 'picocolors';
|
|
5
|
+
import path2, { dirname, join } from 'path';
|
|
8
6
|
import fg from 'fast-glob';
|
|
7
|
+
import { existsSync } from 'fs';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
9
|
import { Hono } from 'hono';
|
|
10
10
|
import { cors } from 'hono/cors';
|
|
11
11
|
|
|
@@ -329,6 +329,241 @@ function debounce(fn, delay) {
|
|
|
329
329
|
}, delay);
|
|
330
330
|
};
|
|
331
331
|
}
|
|
332
|
+
|
|
333
|
+
// src/multi-proxy.ts
|
|
334
|
+
var DEVTOOLS_PROXY_PATH = "/_devtools";
|
|
335
|
+
var API_PROXY_PATH = "/_api";
|
|
336
|
+
var WS_PROXY_PATH = "/_ws";
|
|
337
|
+
function getProxyConfig(vite) {
|
|
338
|
+
if (!vite.config.server) {
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
vite.config.server.proxy ??= {};
|
|
342
|
+
return vite.config.server.proxy;
|
|
343
|
+
}
|
|
344
|
+
function configureMultiProxy(vite, instances, port) {
|
|
345
|
+
const proxyConfig = getProxyConfig(vite);
|
|
346
|
+
if (!proxyConfig) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
const httpTarget = `http://localhost:${port}`;
|
|
350
|
+
for (const instance of instances) {
|
|
351
|
+
const prefix = instance.config.proxyPath;
|
|
352
|
+
proxyConfig[prefix] = {
|
|
353
|
+
target: httpTarget,
|
|
354
|
+
changeOrigin: true,
|
|
355
|
+
rewrite: (path4) => {
|
|
356
|
+
if (path4 !== prefix && !path4.startsWith(`${prefix}/`) && !path4.startsWith(`${prefix}?`))
|
|
357
|
+
return path4;
|
|
358
|
+
const rest = path4.slice(prefix.length);
|
|
359
|
+
if (rest === "" || rest === "/") return "/";
|
|
360
|
+
if (rest.startsWith("?")) return `/${rest}`;
|
|
361
|
+
return rest;
|
|
362
|
+
},
|
|
363
|
+
headers: { "x-spec-id": instance.id }
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
proxyConfig[DEVTOOLS_PROXY_PATH] = {
|
|
367
|
+
target: httpTarget,
|
|
368
|
+
changeOrigin: true
|
|
369
|
+
};
|
|
370
|
+
proxyConfig[API_PROXY_PATH] = {
|
|
371
|
+
target: httpTarget,
|
|
372
|
+
changeOrigin: true
|
|
373
|
+
};
|
|
374
|
+
proxyConfig[WS_PROXY_PATH] = {
|
|
375
|
+
target: `ws://localhost:${port}`,
|
|
376
|
+
changeOrigin: true,
|
|
377
|
+
ws: true
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// src/types.ts
|
|
382
|
+
var ValidationError = class extends Error {
|
|
383
|
+
code;
|
|
384
|
+
constructor(code, message) {
|
|
385
|
+
super(message);
|
|
386
|
+
this.name = "ValidationError";
|
|
387
|
+
this.code = code;
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
function validateSpecs(specs) {
|
|
391
|
+
if (!specs || !Array.isArray(specs) || specs.length === 0) {
|
|
392
|
+
throw new ValidationError(
|
|
393
|
+
"SPECS_EMPTY",
|
|
394
|
+
"specs is required and must be a non-empty array of SpecConfig"
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
for (let i = 0; i < specs.length; i++) {
|
|
398
|
+
const spec = specs[i];
|
|
399
|
+
if (!spec || typeof spec !== "object") {
|
|
400
|
+
throw new ValidationError(
|
|
401
|
+
"SPEC_NOT_FOUND",
|
|
402
|
+
`specs[${i}]: must be a SpecConfig object, got ${spec === null ? "null" : typeof spec}`
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
if (!spec.spec || typeof spec.spec !== "string" || spec.spec.trim() === "") {
|
|
406
|
+
const identifier = spec.id ? ` (id: "${spec.id}")` : "";
|
|
407
|
+
throw new ValidationError(
|
|
408
|
+
"SPEC_NOT_FOUND",
|
|
409
|
+
`specs[${i}]${identifier}: spec field is required and must be a non-empty string (path or URL to OpenAPI spec)`
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
function resolveOptions(options) {
|
|
415
|
+
validateSpecs(options.specs);
|
|
416
|
+
return {
|
|
417
|
+
specs: options.specs.map((s) => ({
|
|
418
|
+
spec: s.spec,
|
|
419
|
+
// Placeholder — populated by orchestrator after document processing
|
|
420
|
+
id: s.id ?? "",
|
|
421
|
+
// Placeholder — populated by orchestrator after document processing
|
|
422
|
+
proxyPath: s.proxyPath ?? "",
|
|
423
|
+
// Preliminary — overwritten by deriveProxyPath() during orchestration
|
|
424
|
+
proxyPathSource: s.proxyPath?.trim() ? "explicit" : "auto",
|
|
425
|
+
handlersDir: s.handlersDir ?? "",
|
|
426
|
+
seedsDir: s.seedsDir ?? "",
|
|
427
|
+
idFields: s.idFields ?? {}
|
|
428
|
+
})),
|
|
429
|
+
port: options.port ?? 4e3,
|
|
430
|
+
enabled: options.enabled ?? true,
|
|
431
|
+
timelineLimit: options.timelineLimit ?? 500,
|
|
432
|
+
devtools: options.devtools ?? true,
|
|
433
|
+
cors: options.cors ?? true,
|
|
434
|
+
corsOrigin: options.corsOrigin ?? "*",
|
|
435
|
+
silent: options.silent ?? false,
|
|
436
|
+
logger: options.logger
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// src/proxy-path.ts
|
|
441
|
+
var RESERVED_PROXY_PATHS = [
|
|
442
|
+
DEVTOOLS_PROXY_PATH,
|
|
443
|
+
API_PROXY_PATH,
|
|
444
|
+
WS_PROXY_PATH
|
|
445
|
+
];
|
|
446
|
+
function deriveProxyPath(explicitPath, document, specId) {
|
|
447
|
+
if (explicitPath.trim()) {
|
|
448
|
+
return {
|
|
449
|
+
proxyPath: normalizeProxyPath(explicitPath.trim(), specId),
|
|
450
|
+
proxyPathSource: "explicit"
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
const servers = document.servers;
|
|
454
|
+
const serverUrl = servers?.[0]?.url?.trim();
|
|
455
|
+
if (!serverUrl) {
|
|
456
|
+
throw new ValidationError(
|
|
457
|
+
"PROXY_PATH_MISSING",
|
|
458
|
+
`[${specId}] Cannot derive proxyPath: no servers defined in the OpenAPI document. Set an explicit proxyPath in the spec configuration.`
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
let path4;
|
|
462
|
+
let parsedUrl;
|
|
463
|
+
try {
|
|
464
|
+
parsedUrl = new URL(serverUrl);
|
|
465
|
+
} catch {
|
|
466
|
+
}
|
|
467
|
+
if (parsedUrl) {
|
|
468
|
+
try {
|
|
469
|
+
path4 = decodeURIComponent(parsedUrl.pathname);
|
|
470
|
+
} catch {
|
|
471
|
+
path4 = parsedUrl.pathname;
|
|
472
|
+
}
|
|
473
|
+
} else {
|
|
474
|
+
path4 = serverUrl;
|
|
475
|
+
}
|
|
476
|
+
return {
|
|
477
|
+
proxyPath: normalizeProxyPath(path4, specId),
|
|
478
|
+
proxyPathSource: "auto"
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
function normalizeProxyPath(path4, specId) {
|
|
482
|
+
path4 = path4.trim();
|
|
483
|
+
const queryIdx = path4.indexOf("?");
|
|
484
|
+
const hashIdx = path4.indexOf("#");
|
|
485
|
+
const cutIdx = Math.min(
|
|
486
|
+
queryIdx >= 0 ? queryIdx : path4.length,
|
|
487
|
+
hashIdx >= 0 ? hashIdx : path4.length
|
|
488
|
+
);
|
|
489
|
+
let normalized = path4.slice(0, cutIdx);
|
|
490
|
+
normalized = normalized.startsWith("/") ? normalized : `/${normalized}`;
|
|
491
|
+
normalized = normalized.replace(/\/{2,}/g, "/");
|
|
492
|
+
const segments = normalized.split("/");
|
|
493
|
+
const resolved = [];
|
|
494
|
+
for (const segment of segments) {
|
|
495
|
+
if (segment === ".") {
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
if (segment === "..") {
|
|
499
|
+
if (resolved.length > 1) {
|
|
500
|
+
resolved.pop();
|
|
501
|
+
}
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
resolved.push(segment);
|
|
505
|
+
}
|
|
506
|
+
normalized = resolved.join("/") || "/";
|
|
507
|
+
if (normalized.length > 1 && normalized.endsWith("/")) {
|
|
508
|
+
normalized = normalized.slice(0, -1);
|
|
509
|
+
}
|
|
510
|
+
if (normalized === "/") {
|
|
511
|
+
throw new ValidationError(
|
|
512
|
+
"PROXY_PATH_TOO_BROAD",
|
|
513
|
+
`[${specId}] proxyPath "/" is too broad \u2014 it would capture all requests. Set a more specific proxyPath (e.g., "/api/v1").`
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
return normalized;
|
|
517
|
+
}
|
|
518
|
+
function validateUniqueProxyPaths(specs) {
|
|
519
|
+
const paths = /* @__PURE__ */ new Map();
|
|
520
|
+
for (const spec of specs) {
|
|
521
|
+
const path4 = spec.proxyPath?.trim();
|
|
522
|
+
if (!path4) {
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
validateNotReservedPath(path4, spec.id);
|
|
526
|
+
if (paths.has(path4)) {
|
|
527
|
+
throw new ValidationError(
|
|
528
|
+
"PROXY_PATH_DUPLICATE",
|
|
529
|
+
`Duplicate proxyPath "${path4}" used by specs "${paths.get(path4)}" and "${spec.id}". Each spec must have a unique proxyPath.`
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
paths.set(path4, spec.id);
|
|
533
|
+
}
|
|
534
|
+
validateNoPrefixOverlaps(paths);
|
|
535
|
+
}
|
|
536
|
+
function validateNotReservedPath(path4, specId) {
|
|
537
|
+
for (const reserved of RESERVED_PROXY_PATHS) {
|
|
538
|
+
if (path4 === reserved || path4.startsWith(`${reserved}/`) || reserved.startsWith(path4)) {
|
|
539
|
+
throw new ValidationError(
|
|
540
|
+
"PROXY_PATH_OVERLAP",
|
|
541
|
+
`[${specId}] proxyPath "${path4}" collides with reserved path "${reserved}" used by the shared DevTools/API/WebSocket service.`
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
function validateNoPrefixOverlaps(paths) {
|
|
547
|
+
const sortedPaths = Array.from(paths.entries()).sort(([a], [b]) => a.length - b.length);
|
|
548
|
+
for (let i = 0; i < sortedPaths.length; i++) {
|
|
549
|
+
for (let j = i + 1; j < sortedPaths.length; j++) {
|
|
550
|
+
const [shorter, shorterId] = sortedPaths[i];
|
|
551
|
+
const [longer, longerId] = sortedPaths[j];
|
|
552
|
+
if (longer.startsWith(`${shorter}/`)) {
|
|
553
|
+
throw new ValidationError(
|
|
554
|
+
"PROXY_PATH_OVERLAP",
|
|
555
|
+
`Overlapping proxyPaths: "${shorter}" (${shorterId}) is a prefix of "${longer}" (${longerId}). This would cause routing ambiguity.`
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
if (longer.startsWith(shorter)) {
|
|
559
|
+
throw new ValidationError(
|
|
560
|
+
"PROXY_PATH_PREFIX_COLLISION",
|
|
561
|
+
`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}".`
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
332
567
|
async function loadSeeds(seedsDir, viteServer, cwd = process.cwd(), logger = console) {
|
|
333
568
|
const seeds = /* @__PURE__ */ new Map();
|
|
334
569
|
const absoluteDir = path2.resolve(cwd, seedsDir);
|
|
@@ -410,480 +645,56 @@ async function getSeedFiles(seedsDir, cwd = process.cwd()) {
|
|
|
410
645
|
return files;
|
|
411
646
|
}
|
|
412
647
|
|
|
413
|
-
// src/
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
648
|
+
// src/spec-id.ts
|
|
649
|
+
function slugify(input) {
|
|
650
|
+
return input.normalize("NFD").replace(new RegExp("\\p{M}", "gu"), "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
651
|
+
}
|
|
652
|
+
function deriveSpecId(explicitId, document) {
|
|
653
|
+
if (explicitId.trim()) {
|
|
654
|
+
const id2 = slugify(explicitId);
|
|
655
|
+
if (!id2) {
|
|
656
|
+
throw new ValidationError(
|
|
657
|
+
"SPEC_ID_MISSING",
|
|
658
|
+
`Cannot derive spec ID: explicit id "${explicitId}" produces an empty slug. Please provide an id containing ASCII letters or numbers.`
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
return id2;
|
|
420
662
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
if (!specs || !Array.isArray(specs) || specs.length === 0) {
|
|
663
|
+
const title = document.info?.title;
|
|
664
|
+
if (!title || !title.trim()) {
|
|
424
665
|
throw new ValidationError(
|
|
425
|
-
"
|
|
426
|
-
"
|
|
666
|
+
"SPEC_ID_MISSING",
|
|
667
|
+
"Cannot derive spec ID: info.title is missing from the OpenAPI document. Please set an explicit id in the spec configuration."
|
|
427
668
|
);
|
|
428
669
|
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
spec: s.spec,
|
|
445
|
-
// Placeholder — populated by orchestrator after document processing
|
|
446
|
-
id: s.id ?? "",
|
|
447
|
-
// Placeholder — populated by orchestrator after document processing
|
|
448
|
-
proxyPath: s.proxyPath ?? "",
|
|
449
|
-
// Preliminary — overwritten by deriveProxyPath() during orchestration
|
|
450
|
-
proxyPathSource: s.proxyPath?.trim() ? "explicit" : "auto",
|
|
451
|
-
handlersDir: s.handlersDir ?? "",
|
|
452
|
-
seedsDir: s.seedsDir ?? "",
|
|
453
|
-
idFields: s.idFields ?? {}
|
|
454
|
-
})),
|
|
455
|
-
port: options.port ?? 4e3,
|
|
456
|
-
enabled: options.enabled ?? true,
|
|
457
|
-
timelineLimit: options.timelineLimit ?? 500,
|
|
458
|
-
devtools: options.devtools ?? true,
|
|
459
|
-
cors: options.cors ?? true,
|
|
460
|
-
corsOrigin: options.corsOrigin ?? "*",
|
|
461
|
-
silent: options.silent ?? false,
|
|
462
|
-
logger: options.logger
|
|
463
|
-
};
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
// src/plugin.ts
|
|
467
|
-
var VIRTUAL_DEVTOOLS_TAB_ID = "virtual:open-api-devtools-tab";
|
|
468
|
-
var RESOLVED_VIRTUAL_DEVTOOLS_TAB_ID = `\0${VIRTUAL_DEVTOOLS_TAB_ID}`;
|
|
469
|
-
function openApiServer(options) {
|
|
470
|
-
const resolvedOptions = resolveOptions(options);
|
|
471
|
-
let server = null;
|
|
472
|
-
let vite = null;
|
|
473
|
-
let fileWatcher = null;
|
|
474
|
-
let cwd = process.cwd();
|
|
475
|
-
return {
|
|
476
|
-
name: "vite-plugin-open-api-server",
|
|
477
|
-
// Only active in dev mode
|
|
478
|
-
apply: "serve",
|
|
479
|
-
/**
|
|
480
|
-
* Ensure @vue/devtools-api is available for the DevTools tab module
|
|
481
|
-
*
|
|
482
|
-
* The virtual module imports from '@vue/devtools-api', which Vite needs
|
|
483
|
-
* to pre-bundle so it can be resolved at runtime in the browser, even
|
|
484
|
-
* though the host app may not have it as a direct dependency.
|
|
485
|
-
*/
|
|
486
|
-
config() {
|
|
487
|
-
if (!resolvedOptions.devtools || !resolvedOptions.enabled) {
|
|
488
|
-
return;
|
|
489
|
-
}
|
|
490
|
-
const require2 = createRequire(import.meta.url);
|
|
491
|
-
try {
|
|
492
|
-
require2.resolve("@vue/devtools-api");
|
|
493
|
-
} catch {
|
|
494
|
-
return;
|
|
495
|
-
}
|
|
496
|
-
return {
|
|
497
|
-
optimizeDeps: {
|
|
498
|
-
include: ["@vue/devtools-api"]
|
|
499
|
-
}
|
|
500
|
-
};
|
|
501
|
-
},
|
|
502
|
-
/**
|
|
503
|
-
* Resolve the virtual module for DevTools tab registration
|
|
504
|
-
*/
|
|
505
|
-
resolveId(id) {
|
|
506
|
-
if (id === VIRTUAL_DEVTOOLS_TAB_ID) {
|
|
507
|
-
return RESOLVED_VIRTUAL_DEVTOOLS_TAB_ID;
|
|
508
|
-
}
|
|
509
|
-
},
|
|
510
|
-
/**
|
|
511
|
-
* Load the virtual module that registers the custom Vue DevTools tab
|
|
512
|
-
*
|
|
513
|
-
* This code is served as a proper Vite module, allowing bare import
|
|
514
|
-
* specifiers to be resolved through Vite's dependency pre-bundling.
|
|
515
|
-
*/
|
|
516
|
-
load(id) {
|
|
517
|
-
if (id === RESOLVED_VIRTUAL_DEVTOOLS_TAB_ID) {
|
|
518
|
-
return `
|
|
519
|
-
import { addCustomTab } from '@vue/devtools-api';
|
|
520
|
-
|
|
521
|
-
try {
|
|
522
|
-
// Route through Vite's proxy so it works in all environments
|
|
523
|
-
const iframeSrc = window.location.origin + '/_devtools/';
|
|
524
|
-
|
|
525
|
-
addCustomTab({
|
|
526
|
-
name: 'vite-plugin-open-api-server',
|
|
527
|
-
title: 'OpenAPI Server',
|
|
528
|
-
icon: 'https://api.iconify.design/carbon:api-1.svg?width=24&height=24&color=%2394a3b8',
|
|
529
|
-
view: {
|
|
530
|
-
type: 'iframe',
|
|
531
|
-
src: iframeSrc,
|
|
532
|
-
},
|
|
533
|
-
category: 'app',
|
|
534
|
-
});
|
|
535
|
-
} catch (e) {
|
|
536
|
-
// @vue/devtools-api not available - expected when the package is not installed
|
|
537
|
-
}
|
|
538
|
-
`;
|
|
539
|
-
}
|
|
540
|
-
},
|
|
541
|
-
/**
|
|
542
|
-
* Configure the Vite dev server
|
|
543
|
-
*
|
|
544
|
-
* This hook is called when the dev server is created.
|
|
545
|
-
* We use it to:
|
|
546
|
-
* 1. Start the OpenAPI mock server
|
|
547
|
-
* 2. Configure the Vite proxy to forward API requests
|
|
548
|
-
* 3. Set up file watching for hot reload
|
|
549
|
-
*/
|
|
550
|
-
async configureServer(viteServer) {
|
|
551
|
-
vite = viteServer;
|
|
552
|
-
cwd = viteServer.config.root;
|
|
553
|
-
if (!resolvedOptions.enabled) {
|
|
554
|
-
return;
|
|
555
|
-
}
|
|
556
|
-
try {
|
|
557
|
-
const handlersResult = await loadHandlers(resolvedOptions.handlersDir, viteServer, cwd);
|
|
558
|
-
const seedsResult = await loadSeeds(resolvedOptions.seedsDir, viteServer, cwd);
|
|
559
|
-
let devtoolsSpaDir;
|
|
560
|
-
if (resolvedOptions.devtools) {
|
|
561
|
-
const pluginDir = dirname(fileURLToPath(import.meta.url));
|
|
562
|
-
const spaDir = join(pluginDir, "devtools-spa");
|
|
563
|
-
if (existsSync(spaDir)) {
|
|
564
|
-
devtoolsSpaDir = spaDir;
|
|
565
|
-
} else {
|
|
566
|
-
resolvedOptions.logger?.warn?.(
|
|
567
|
-
"[vite-plugin-open-api-server] DevTools SPA not found at",
|
|
568
|
-
spaDir,
|
|
569
|
-
'- serving placeholder. Run "pnpm build" to include the SPA.'
|
|
570
|
-
);
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
const opts = resolvedOptions;
|
|
574
|
-
server = await createOpenApiServer({
|
|
575
|
-
spec: opts.spec,
|
|
576
|
-
port: opts.port,
|
|
577
|
-
idFields: opts.idFields,
|
|
578
|
-
handlers: handlersResult.handlers,
|
|
579
|
-
// Seeds are populated via executeSeeds, not directly
|
|
580
|
-
seeds: /* @__PURE__ */ new Map(),
|
|
581
|
-
timelineLimit: opts.timelineLimit,
|
|
582
|
-
devtools: opts.devtools,
|
|
583
|
-
devtoolsSpaDir,
|
|
584
|
-
cors: opts.cors,
|
|
585
|
-
corsOrigin: opts.corsOrigin,
|
|
586
|
-
logger: opts.logger
|
|
587
|
-
});
|
|
588
|
-
if (seedsResult.seeds.size > 0) {
|
|
589
|
-
await executeSeeds(seedsResult.seeds, server.store, server.document);
|
|
590
|
-
}
|
|
591
|
-
await server.start();
|
|
592
|
-
configureProxy(viteServer, resolvedOptions.proxyPath, resolvedOptions.port);
|
|
593
|
-
const bannerInfo = extractBannerInfo(
|
|
594
|
-
server.registry,
|
|
595
|
-
{
|
|
596
|
-
info: {
|
|
597
|
-
title: server.document.info?.title ?? "OpenAPI Server",
|
|
598
|
-
version: server.document.info?.version ?? "1.0.0"
|
|
599
|
-
}
|
|
600
|
-
},
|
|
601
|
-
handlersResult.handlers.size,
|
|
602
|
-
seedsResult.seeds.size,
|
|
603
|
-
resolvedOptions
|
|
604
|
-
);
|
|
605
|
-
printBanner(bannerInfo, resolvedOptions);
|
|
606
|
-
await setupFileWatching();
|
|
607
|
-
} catch (error) {
|
|
608
|
-
printError("Failed to start OpenAPI mock server", error, resolvedOptions);
|
|
609
|
-
throw error;
|
|
610
|
-
}
|
|
611
|
-
},
|
|
612
|
-
/**
|
|
613
|
-
* Clean up when Vite server closes
|
|
614
|
-
*/
|
|
615
|
-
async closeBundle() {
|
|
616
|
-
await cleanup();
|
|
617
|
-
},
|
|
618
|
-
/**
|
|
619
|
-
* Inject Vue DevTools custom tab registration script
|
|
620
|
-
*
|
|
621
|
-
* When devtools is enabled, this injects a script tag that loads the
|
|
622
|
-
* virtual module for custom tab registration. Using a virtual module
|
|
623
|
-
* (instead of an inline script) ensures that bare import specifiers
|
|
624
|
-
* like `@vue/devtools-api` are resolved through Vite's module pipeline.
|
|
625
|
-
*/
|
|
626
|
-
transformIndexHtml() {
|
|
627
|
-
if (!resolvedOptions.devtools || !resolvedOptions.enabled) {
|
|
628
|
-
return;
|
|
629
|
-
}
|
|
630
|
-
return [
|
|
631
|
-
{
|
|
632
|
-
tag: "script",
|
|
633
|
-
attrs: { type: "module", src: `/@id/${VIRTUAL_DEVTOOLS_TAB_ID}` },
|
|
634
|
-
injectTo: "head"
|
|
635
|
-
}
|
|
636
|
-
];
|
|
637
|
-
}
|
|
638
|
-
};
|
|
639
|
-
function configureProxy(vite2, proxyPath, port) {
|
|
640
|
-
const serverConfig = vite2.config.server ?? {};
|
|
641
|
-
const proxyConfig = serverConfig.proxy ?? {};
|
|
642
|
-
const escapedPath = proxyPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
643
|
-
const pathPrefixRegex = new RegExp(`^${escapedPath}`);
|
|
644
|
-
proxyConfig[proxyPath] = {
|
|
645
|
-
target: `http://localhost:${port}`,
|
|
646
|
-
changeOrigin: true,
|
|
647
|
-
// Remove the proxy path prefix when forwarding
|
|
648
|
-
rewrite: (path4) => path4.replace(pathPrefixRegex, "")
|
|
649
|
-
};
|
|
650
|
-
proxyConfig["/_devtools"] = {
|
|
651
|
-
target: `http://localhost:${port}`,
|
|
652
|
-
changeOrigin: true
|
|
653
|
-
};
|
|
654
|
-
proxyConfig["/_api"] = {
|
|
655
|
-
target: `http://localhost:${port}`,
|
|
656
|
-
changeOrigin: true
|
|
657
|
-
};
|
|
658
|
-
proxyConfig["/_ws"] = {
|
|
659
|
-
target: `http://localhost:${port}`,
|
|
660
|
-
changeOrigin: true,
|
|
661
|
-
ws: true
|
|
662
|
-
};
|
|
663
|
-
if (vite2.config.server) {
|
|
664
|
-
vite2.config.server.proxy = proxyConfig;
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
async function setupFileWatching() {
|
|
668
|
-
if (!server || !vite) return;
|
|
669
|
-
const debouncedHandlerReload = debounce(reloadHandlers, 100);
|
|
670
|
-
const debouncedSeedReload = debounce(reloadSeeds, 100);
|
|
671
|
-
const watchOpts = resolvedOptions;
|
|
672
|
-
fileWatcher = await createFileWatcher({
|
|
673
|
-
handlersDir: watchOpts.handlersDir,
|
|
674
|
-
seedsDir: watchOpts.seedsDir,
|
|
675
|
-
cwd,
|
|
676
|
-
onHandlerChange: debouncedHandlerReload,
|
|
677
|
-
onSeedChange: debouncedSeedReload
|
|
678
|
-
});
|
|
679
|
-
}
|
|
680
|
-
async function reloadHandlers() {
|
|
681
|
-
if (!server || !vite) return;
|
|
682
|
-
try {
|
|
683
|
-
const handlersResult = await loadHandlers(
|
|
684
|
-
// biome-ignore lint/suspicious/noExplicitAny: v0.x compat — plugin.ts rewritten in Task 1.7
|
|
685
|
-
resolvedOptions.handlersDir,
|
|
686
|
-
vite,
|
|
687
|
-
cwd
|
|
688
|
-
);
|
|
689
|
-
server.updateHandlers(handlersResult.handlers);
|
|
690
|
-
server.wsHub.broadcast({
|
|
691
|
-
type: "handlers:updated",
|
|
692
|
-
data: { count: handlersResult.handlers.size }
|
|
693
|
-
});
|
|
694
|
-
printReloadNotification("handlers", handlersResult.handlers.size, resolvedOptions);
|
|
695
|
-
} catch (error) {
|
|
696
|
-
printError("Failed to reload handlers", error, resolvedOptions);
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
async function reloadSeeds() {
|
|
700
|
-
if (!server || !vite) return;
|
|
701
|
-
try {
|
|
702
|
-
const seedsResult = await loadSeeds(
|
|
703
|
-
// biome-ignore lint/suspicious/noExplicitAny: v0.x compat — plugin.ts rewritten in Task 1.7
|
|
704
|
-
resolvedOptions.seedsDir,
|
|
705
|
-
vite,
|
|
706
|
-
cwd
|
|
707
|
-
);
|
|
708
|
-
if (seedsResult.seeds.size > 0) {
|
|
709
|
-
server.store.clearAll();
|
|
710
|
-
await executeSeeds(seedsResult.seeds, server.store, server.document);
|
|
711
|
-
} else {
|
|
712
|
-
server.store.clearAll();
|
|
713
|
-
}
|
|
714
|
-
server.wsHub.broadcast({
|
|
715
|
-
type: "seeds:updated",
|
|
716
|
-
data: { count: seedsResult.seeds.size }
|
|
717
|
-
});
|
|
718
|
-
printReloadNotification("seeds", seedsResult.seeds.size, resolvedOptions);
|
|
719
|
-
} catch (error) {
|
|
720
|
-
printError("Failed to reload seeds", error, resolvedOptions);
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
async function cleanup() {
|
|
724
|
-
if (fileWatcher) {
|
|
725
|
-
await fileWatcher.close();
|
|
726
|
-
fileWatcher = null;
|
|
727
|
-
}
|
|
728
|
-
if (server) {
|
|
729
|
-
await server.stop();
|
|
730
|
-
server = null;
|
|
731
|
-
}
|
|
732
|
-
vite = null;
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
// src/spec-id.ts
|
|
737
|
-
function slugify(input) {
|
|
738
|
-
return input.normalize("NFD").replace(new RegExp("\\p{M}", "gu"), "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
739
|
-
}
|
|
740
|
-
function deriveSpecId(explicitId, document) {
|
|
741
|
-
if (explicitId.trim()) {
|
|
742
|
-
const id2 = slugify(explicitId);
|
|
743
|
-
if (!id2) {
|
|
744
|
-
throw new ValidationError(
|
|
745
|
-
"SPEC_ID_MISSING",
|
|
746
|
-
`Cannot derive spec ID: explicit id "${explicitId}" produces an empty slug. Please provide an id containing ASCII letters or numbers.`
|
|
747
|
-
);
|
|
748
|
-
}
|
|
749
|
-
return id2;
|
|
750
|
-
}
|
|
751
|
-
const title = document.info?.title;
|
|
752
|
-
if (!title || !title.trim()) {
|
|
753
|
-
throw new ValidationError(
|
|
754
|
-
"SPEC_ID_MISSING",
|
|
755
|
-
"Cannot derive spec ID: info.title is missing from the OpenAPI document. Please set an explicit id in the spec configuration."
|
|
756
|
-
);
|
|
757
|
-
}
|
|
758
|
-
const id = slugify(title);
|
|
759
|
-
if (!id) {
|
|
760
|
-
throw new ValidationError(
|
|
761
|
-
"SPEC_ID_MISSING",
|
|
762
|
-
`Cannot derive spec ID: info.title "${title}" produces an empty slug. Please set an explicit id in the spec configuration.`
|
|
763
|
-
);
|
|
764
|
-
}
|
|
765
|
-
return id;
|
|
766
|
-
}
|
|
767
|
-
function validateUniqueIds(ids) {
|
|
768
|
-
const seen = /* @__PURE__ */ new Set();
|
|
769
|
-
const duplicates = /* @__PURE__ */ new Set();
|
|
770
|
-
for (const id of ids) {
|
|
771
|
-
if (seen.has(id)) {
|
|
772
|
-
duplicates.add(id);
|
|
773
|
-
}
|
|
774
|
-
seen.add(id);
|
|
775
|
-
}
|
|
776
|
-
if (duplicates.size > 0) {
|
|
777
|
-
const list = [...duplicates].join(", ");
|
|
778
|
-
throw new ValidationError(
|
|
779
|
-
"SPEC_ID_DUPLICATE",
|
|
780
|
-
`Duplicate spec IDs: ${list}. Each spec must have a unique ID. Set explicit ids in spec configuration to resolve.`
|
|
781
|
-
);
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
// src/proxy-path.ts
|
|
786
|
-
function deriveProxyPath(explicitPath, document, specId) {
|
|
787
|
-
if (explicitPath.trim()) {
|
|
788
|
-
return {
|
|
789
|
-
proxyPath: normalizeProxyPath(explicitPath.trim(), specId),
|
|
790
|
-
proxyPathSource: "explicit"
|
|
791
|
-
};
|
|
792
|
-
}
|
|
793
|
-
const servers = document.servers;
|
|
794
|
-
const serverUrl = servers?.[0]?.url?.trim();
|
|
795
|
-
if (!serverUrl) {
|
|
796
|
-
throw new ValidationError(
|
|
797
|
-
"PROXY_PATH_MISSING",
|
|
798
|
-
`[${specId}] Cannot derive proxyPath: no servers defined in the OpenAPI document. Set an explicit proxyPath in the spec configuration.`
|
|
799
|
-
);
|
|
800
|
-
}
|
|
801
|
-
let path4;
|
|
802
|
-
let parsedUrl;
|
|
803
|
-
try {
|
|
804
|
-
parsedUrl = new URL(serverUrl);
|
|
805
|
-
} catch {
|
|
806
|
-
}
|
|
807
|
-
if (parsedUrl) {
|
|
808
|
-
try {
|
|
809
|
-
path4 = decodeURIComponent(parsedUrl.pathname);
|
|
810
|
-
} catch {
|
|
811
|
-
path4 = parsedUrl.pathname;
|
|
812
|
-
}
|
|
813
|
-
} else {
|
|
814
|
-
path4 = serverUrl;
|
|
815
|
-
}
|
|
816
|
-
return {
|
|
817
|
-
proxyPath: normalizeProxyPath(path4, specId),
|
|
818
|
-
proxyPathSource: "auto"
|
|
819
|
-
};
|
|
820
|
-
}
|
|
821
|
-
function normalizeProxyPath(path4, specId) {
|
|
822
|
-
path4 = path4.trim();
|
|
823
|
-
const queryIdx = path4.indexOf("?");
|
|
824
|
-
const hashIdx = path4.indexOf("#");
|
|
825
|
-
const cutIdx = Math.min(
|
|
826
|
-
queryIdx >= 0 ? queryIdx : path4.length,
|
|
827
|
-
hashIdx >= 0 ? hashIdx : path4.length
|
|
828
|
-
);
|
|
829
|
-
let normalized = path4.slice(0, cutIdx);
|
|
830
|
-
normalized = normalized.startsWith("/") ? normalized : `/${normalized}`;
|
|
831
|
-
normalized = normalized.replace(/\/{2,}/g, "/");
|
|
832
|
-
const segments = normalized.split("/");
|
|
833
|
-
const resolved = [];
|
|
834
|
-
for (const segment of segments) {
|
|
835
|
-
if (segment === ".") {
|
|
836
|
-
continue;
|
|
837
|
-
}
|
|
838
|
-
if (segment === "..") {
|
|
839
|
-
if (resolved.length > 1) {
|
|
840
|
-
resolved.pop();
|
|
841
|
-
}
|
|
842
|
-
continue;
|
|
843
|
-
}
|
|
844
|
-
resolved.push(segment);
|
|
845
|
-
}
|
|
846
|
-
normalized = resolved.join("/") || "/";
|
|
847
|
-
if (normalized.length > 1 && normalized.endsWith("/")) {
|
|
848
|
-
normalized = normalized.slice(0, -1);
|
|
849
|
-
}
|
|
850
|
-
if (normalized === "/") {
|
|
851
|
-
throw new ValidationError(
|
|
852
|
-
"PROXY_PATH_TOO_BROAD",
|
|
853
|
-
`[${specId}] proxyPath "/" is too broad \u2014 it would capture all requests. Set a more specific proxyPath (e.g., "/api/v1").`
|
|
854
|
-
);
|
|
855
|
-
}
|
|
856
|
-
return normalized;
|
|
857
|
-
}
|
|
858
|
-
function validateUniqueProxyPaths(specs) {
|
|
859
|
-
const paths = /* @__PURE__ */ new Map();
|
|
860
|
-
for (const spec of specs) {
|
|
861
|
-
const path4 = spec.proxyPath?.trim();
|
|
862
|
-
if (!path4) {
|
|
863
|
-
continue;
|
|
864
|
-
}
|
|
865
|
-
if (paths.has(path4)) {
|
|
866
|
-
throw new ValidationError(
|
|
867
|
-
"PROXY_PATH_DUPLICATE",
|
|
868
|
-
`Duplicate proxyPath "${path4}" used by specs "${paths.get(path4)}" and "${spec.id}". Each spec must have a unique proxyPath.`
|
|
869
|
-
);
|
|
870
|
-
}
|
|
871
|
-
paths.set(path4, spec.id);
|
|
872
|
-
}
|
|
873
|
-
const sortedPaths = Array.from(paths.entries()).sort(([a], [b]) => a.length - b.length);
|
|
874
|
-
for (let i = 0; i < sortedPaths.length; i++) {
|
|
875
|
-
for (let j = i + 1; j < sortedPaths.length; j++) {
|
|
876
|
-
const [shorter, shorterId] = sortedPaths[i];
|
|
877
|
-
const [longer, longerId] = sortedPaths[j];
|
|
878
|
-
if (longer.startsWith(`${shorter}/`)) {
|
|
879
|
-
throw new ValidationError(
|
|
880
|
-
"PROXY_PATH_OVERLAP",
|
|
881
|
-
`Overlapping proxyPaths: "${shorter}" (${shorterId}) is a prefix of "${longer}" (${longerId}). This would cause routing ambiguity.`
|
|
882
|
-
);
|
|
883
|
-
}
|
|
670
|
+
const id = slugify(title);
|
|
671
|
+
if (!id) {
|
|
672
|
+
throw new ValidationError(
|
|
673
|
+
"SPEC_ID_MISSING",
|
|
674
|
+
`Cannot derive spec ID: info.title "${title}" produces an empty slug. Please set an explicit id in the spec configuration.`
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
return id;
|
|
678
|
+
}
|
|
679
|
+
function validateUniqueIds(ids) {
|
|
680
|
+
const seen = /* @__PURE__ */ new Set();
|
|
681
|
+
const duplicates = /* @__PURE__ */ new Set();
|
|
682
|
+
for (const id of ids) {
|
|
683
|
+
if (seen.has(id)) {
|
|
684
|
+
duplicates.add(id);
|
|
884
685
|
}
|
|
686
|
+
seen.add(id);
|
|
687
|
+
}
|
|
688
|
+
if (duplicates.size > 0) {
|
|
689
|
+
const list = [...duplicates].join(", ");
|
|
690
|
+
throw new ValidationError(
|
|
691
|
+
"SPEC_ID_DUPLICATE",
|
|
692
|
+
`Duplicate spec IDs: ${list}. Each spec must have a unique ID. Set explicit ids in spec configuration to resolve.`
|
|
693
|
+
);
|
|
885
694
|
}
|
|
886
695
|
}
|
|
696
|
+
|
|
697
|
+
// src/orchestrator.ts
|
|
887
698
|
var SPEC_COLORS = [
|
|
888
699
|
"#4ade80",
|
|
889
700
|
// green
|
|
@@ -942,7 +753,7 @@ async function processSpec(specConfig, index, options, vite, cwd, logger) {
|
|
|
942
753
|
}
|
|
943
754
|
function buildCorsConfig(options) {
|
|
944
755
|
const isWildcardOrigin = options.corsOrigin === "*" || Array.isArray(options.corsOrigin) && options.corsOrigin.includes("*");
|
|
945
|
-
const effectiveCorsOrigin =
|
|
756
|
+
const effectiveCorsOrigin = isWildcardOrigin ? "*" : options.corsOrigin;
|
|
946
757
|
return {
|
|
947
758
|
origin: effectiveCorsOrigin,
|
|
948
759
|
allowMethods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"],
|
|
@@ -999,12 +810,11 @@ async function createOrchestrator(options, vite, cwd) {
|
|
|
999
810
|
cwd,
|
|
1000
811
|
logger
|
|
1001
812
|
);
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
specConfig.seedsDir = resolvedConfig.seedsDir;
|
|
813
|
+
instance.config.id = resolvedConfig.id;
|
|
814
|
+
instance.config.proxyPath = resolvedConfig.proxyPath;
|
|
815
|
+
instance.config.proxyPathSource = resolvedConfig.proxyPathSource;
|
|
816
|
+
instance.config.handlersDir = resolvedConfig.handlersDir;
|
|
817
|
+
instance.config.seedsDir = resolvedConfig.seedsDir;
|
|
1008
818
|
instances.push(instance);
|
|
1009
819
|
}
|
|
1010
820
|
validateUniqueIds(instances.map((inst) => inst.id));
|
|
@@ -1025,12 +835,16 @@ async function createOrchestrator(options, vite, cwd) {
|
|
|
1025
835
|
if (options.devtools) {
|
|
1026
836
|
mountDevToolsSpa(mainApp, logger);
|
|
1027
837
|
}
|
|
838
|
+
if (instances.length > 1) {
|
|
839
|
+
logger.warn?.(
|
|
840
|
+
"[vite-plugin-open-api-server] Only first spec's internal API mounted on /_api; multi-spec support planned in Epic 3 (Task 3.x)."
|
|
841
|
+
);
|
|
842
|
+
mainApp.use("/_api/*", async (c, next) => {
|
|
843
|
+
await next();
|
|
844
|
+
c.header("X-Multi-Spec-Warning", `Only showing data for spec "${instances[0].id}"`);
|
|
845
|
+
});
|
|
846
|
+
}
|
|
1028
847
|
if (instances.length > 0) {
|
|
1029
|
-
if (instances.length > 1) {
|
|
1030
|
-
logger.warn?.(
|
|
1031
|
-
"[vite-plugin-open-api-server] Only first spec's internal API mounted on /_api; multi-spec support planned in Epic 3 (Task 3.x)."
|
|
1032
|
-
);
|
|
1033
|
-
}
|
|
1034
848
|
const firstInstance = instances[0];
|
|
1035
849
|
mountInternalApi(mainApp, {
|
|
1036
850
|
store: firstInstance.server.store,
|
|
@@ -1048,6 +862,37 @@ async function createOrchestrator(options, vite, cwd) {
|
|
|
1048
862
|
const specsInfo = instances.map((inst) => inst.info);
|
|
1049
863
|
let serverInstance = null;
|
|
1050
864
|
let boundPort = 0;
|
|
865
|
+
async function startServerOnPort(fetchHandler, port) {
|
|
866
|
+
let createAdaptorServer;
|
|
867
|
+
try {
|
|
868
|
+
const nodeServer = await import('@hono/node-server');
|
|
869
|
+
createAdaptorServer = nodeServer.createAdaptorServer;
|
|
870
|
+
} catch {
|
|
871
|
+
throw new Error("@hono/node-server is required. Install with: npm install @hono/node-server");
|
|
872
|
+
}
|
|
873
|
+
const server = createAdaptorServer({ fetch: fetchHandler });
|
|
874
|
+
const actualPort = await new Promise((resolve, reject) => {
|
|
875
|
+
const onListening = () => {
|
|
876
|
+
server.removeListener("error", onError);
|
|
877
|
+
const addr = server.address();
|
|
878
|
+
resolve(typeof addr === "object" && addr ? addr.port : port);
|
|
879
|
+
};
|
|
880
|
+
const onError = (err) => {
|
|
881
|
+
server.removeListener("listening", onListening);
|
|
882
|
+
server.close(() => {
|
|
883
|
+
});
|
|
884
|
+
if (err.code === "EADDRINUSE") {
|
|
885
|
+
reject(new Error(`[vite-plugin-open-api-server] Port ${port} is already in use.`));
|
|
886
|
+
} else {
|
|
887
|
+
reject(new Error(`[vite-plugin-open-api-server] Server error: ${err.message}`));
|
|
888
|
+
}
|
|
889
|
+
};
|
|
890
|
+
server.once("listening", onListening);
|
|
891
|
+
server.once("error", onError);
|
|
892
|
+
server.listen(port);
|
|
893
|
+
});
|
|
894
|
+
return { server, actualPort };
|
|
895
|
+
}
|
|
1051
896
|
return {
|
|
1052
897
|
app: mainApp,
|
|
1053
898
|
instances,
|
|
@@ -1059,51 +904,18 @@ async function createOrchestrator(options, vite, cwd) {
|
|
|
1059
904
|
if (serverInstance) {
|
|
1060
905
|
throw new Error("[vite-plugin-open-api-server] Server already running. Call stop() first.");
|
|
1061
906
|
}
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
} catch {
|
|
1067
|
-
throw new Error(
|
|
1068
|
-
"@hono/node-server is required. Install with: npm install @hono/node-server"
|
|
1069
|
-
);
|
|
1070
|
-
}
|
|
1071
|
-
const server = createAdaptorServer({ fetch: mainApp.fetch });
|
|
1072
|
-
await new Promise((resolve, reject) => {
|
|
1073
|
-
const onListening = () => {
|
|
1074
|
-
server.removeListener("error", onError);
|
|
1075
|
-
const addr = server.address();
|
|
1076
|
-
const actualPort = typeof addr === "object" && addr ? addr.port : options.port;
|
|
1077
|
-
logger.info(
|
|
1078
|
-
`[vite-plugin-open-api-server] Server started on http://localhost:${actualPort}`
|
|
1079
|
-
);
|
|
1080
|
-
boundPort = actualPort;
|
|
1081
|
-
serverInstance = server;
|
|
1082
|
-
resolve();
|
|
1083
|
-
};
|
|
1084
|
-
const onError = (err) => {
|
|
1085
|
-
server.removeListener("listening", onListening);
|
|
1086
|
-
server.close(() => {
|
|
1087
|
-
});
|
|
1088
|
-
serverInstance = null;
|
|
1089
|
-
boundPort = 0;
|
|
1090
|
-
if (err.code === "EADDRINUSE") {
|
|
1091
|
-
reject(
|
|
1092
|
-
new Error(`[vite-plugin-open-api-server] Port ${options.port} is already in use.`)
|
|
1093
|
-
);
|
|
1094
|
-
} else {
|
|
1095
|
-
reject(new Error(`[vite-plugin-open-api-server] Server error: ${err.message}`));
|
|
1096
|
-
}
|
|
1097
|
-
};
|
|
1098
|
-
server.once("listening", onListening);
|
|
1099
|
-
server.once("error", onError);
|
|
1100
|
-
server.listen(options.port);
|
|
1101
|
-
});
|
|
907
|
+
const { server, actualPort } = await startServerOnPort(mainApp.fetch, options.port);
|
|
908
|
+
serverInstance = server;
|
|
909
|
+
boundPort = actualPort;
|
|
910
|
+
logger.info(`[vite-plugin-open-api-server] Server started on http://localhost:${actualPort}`);
|
|
1102
911
|
},
|
|
1103
912
|
async stop() {
|
|
1104
913
|
const server = serverInstance;
|
|
1105
914
|
if (server) {
|
|
1106
915
|
try {
|
|
916
|
+
if (typeof server.closeAllConnections === "function") {
|
|
917
|
+
server.closeAllConnections();
|
|
918
|
+
}
|
|
1107
919
|
await new Promise((resolve, reject) => {
|
|
1108
920
|
server.close((err) => {
|
|
1109
921
|
if (err) {
|
|
@@ -1126,6 +938,229 @@ async function createOrchestrator(options, vite, cwd) {
|
|
|
1126
938
|
};
|
|
1127
939
|
}
|
|
1128
940
|
|
|
941
|
+
// src/plugin.ts
|
|
942
|
+
var VIRTUAL_DEVTOOLS_TAB_ID = "virtual:open-api-devtools-tab";
|
|
943
|
+
var RESOLVED_VIRTUAL_DEVTOOLS_TAB_ID = `\0${VIRTUAL_DEVTOOLS_TAB_ID}`;
|
|
944
|
+
function openApiServer(options) {
|
|
945
|
+
const resolvedOptions = resolveOptions(options);
|
|
946
|
+
let orchestrator = null;
|
|
947
|
+
let fileWatchers = [];
|
|
948
|
+
let cwd = process.cwd();
|
|
949
|
+
return {
|
|
950
|
+
name: "vite-plugin-open-api-server",
|
|
951
|
+
apply: "serve",
|
|
952
|
+
/**
|
|
953
|
+
* Ensure @vue/devtools-api is available for the DevTools tab module
|
|
954
|
+
*
|
|
955
|
+
* The virtual module imports from '@vue/devtools-api', which Vite needs
|
|
956
|
+
* to pre-bundle so it can be resolved at runtime in the browser, even
|
|
957
|
+
* though the host app may not have it as a direct dependency.
|
|
958
|
+
*/
|
|
959
|
+
config() {
|
|
960
|
+
if (!resolvedOptions.devtools || !resolvedOptions.enabled) {
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
const require2 = createRequire(import.meta.url);
|
|
964
|
+
try {
|
|
965
|
+
require2.resolve("@vue/devtools-api");
|
|
966
|
+
} catch {
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
return {
|
|
970
|
+
optimizeDeps: {
|
|
971
|
+
include: ["@vue/devtools-api"]
|
|
972
|
+
}
|
|
973
|
+
};
|
|
974
|
+
},
|
|
975
|
+
/**
|
|
976
|
+
* Resolve the virtual module for DevTools tab registration
|
|
977
|
+
*/
|
|
978
|
+
resolveId(id) {
|
|
979
|
+
if (id === VIRTUAL_DEVTOOLS_TAB_ID) {
|
|
980
|
+
return RESOLVED_VIRTUAL_DEVTOOLS_TAB_ID;
|
|
981
|
+
}
|
|
982
|
+
},
|
|
983
|
+
/**
|
|
984
|
+
* Load the virtual module that registers the custom Vue DevTools tab
|
|
985
|
+
*
|
|
986
|
+
* This code is served as a proper Vite module, allowing bare import
|
|
987
|
+
* specifiers to be resolved through Vite's dependency pre-bundling.
|
|
988
|
+
*/
|
|
989
|
+
load(id) {
|
|
990
|
+
if (id === RESOLVED_VIRTUAL_DEVTOOLS_TAB_ID) {
|
|
991
|
+
return `
|
|
992
|
+
import { addCustomTab } from '@vue/devtools-api';
|
|
993
|
+
|
|
994
|
+
try {
|
|
995
|
+
// Route through Vite's proxy so it works in all environments
|
|
996
|
+
const iframeSrc = window.location.origin + '${DEVTOOLS_PROXY_PATH}/';
|
|
997
|
+
|
|
998
|
+
addCustomTab({
|
|
999
|
+
name: 'vite-plugin-open-api-server',
|
|
1000
|
+
title: 'OpenAPI Server',
|
|
1001
|
+
icon: 'https://api.iconify.design/carbon:api-1.svg?width=24&height=24&color=%2394a3b8',
|
|
1002
|
+
view: {
|
|
1003
|
+
type: 'iframe',
|
|
1004
|
+
src: iframeSrc,
|
|
1005
|
+
},
|
|
1006
|
+
category: 'app',
|
|
1007
|
+
});
|
|
1008
|
+
} catch (e) {
|
|
1009
|
+
// @vue/devtools-api not available - expected when the package is not installed
|
|
1010
|
+
}
|
|
1011
|
+
`;
|
|
1012
|
+
}
|
|
1013
|
+
},
|
|
1014
|
+
/**
|
|
1015
|
+
* Configure the Vite dev server
|
|
1016
|
+
*
|
|
1017
|
+
* This hook is called when the dev server is created.
|
|
1018
|
+
* We use it to:
|
|
1019
|
+
* 1. Create and start the multi-spec orchestrator
|
|
1020
|
+
* 2. Configure Vite's multi-proxy for all specs
|
|
1021
|
+
* 3. Set up per-spec file watchers for hot reload
|
|
1022
|
+
* 4. Print startup banner
|
|
1023
|
+
*/
|
|
1024
|
+
async configureServer(viteServer) {
|
|
1025
|
+
cwd = viteServer.config.root;
|
|
1026
|
+
if (!resolvedOptions.enabled) {
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
try {
|
|
1030
|
+
orchestrator = await createOrchestrator(resolvedOptions, viteServer, cwd);
|
|
1031
|
+
await orchestrator.start();
|
|
1032
|
+
configureMultiProxy(viteServer, orchestrator.instances, orchestrator.port);
|
|
1033
|
+
fileWatchers = await setupPerSpecFileWatching(orchestrator, viteServer, cwd);
|
|
1034
|
+
if (!resolvedOptions.silent && orchestrator.instances.length > 0) {
|
|
1035
|
+
const firstInstance = orchestrator.instances[0];
|
|
1036
|
+
const bannerInfo = extractBannerInfo(
|
|
1037
|
+
firstInstance.server.registry,
|
|
1038
|
+
{
|
|
1039
|
+
info: {
|
|
1040
|
+
title: firstInstance.server.document.info?.title ?? "OpenAPI Server",
|
|
1041
|
+
version: firstInstance.server.document.info?.version ?? "1.0.0"
|
|
1042
|
+
}
|
|
1043
|
+
},
|
|
1044
|
+
// Handler/seed counts are not tracked per-instance yet.
|
|
1045
|
+
// Multi-spec banner (Epic 5, Task 5.1) will display proper per-spec counts.
|
|
1046
|
+
0,
|
|
1047
|
+
0,
|
|
1048
|
+
resolvedOptions
|
|
1049
|
+
);
|
|
1050
|
+
printBanner(bannerInfo, resolvedOptions);
|
|
1051
|
+
}
|
|
1052
|
+
} catch (error) {
|
|
1053
|
+
await teardown();
|
|
1054
|
+
printError("Failed to start OpenAPI mock server", error, resolvedOptions);
|
|
1055
|
+
throw error;
|
|
1056
|
+
}
|
|
1057
|
+
},
|
|
1058
|
+
/**
|
|
1059
|
+
* Inject Vue DevTools custom tab registration script
|
|
1060
|
+
*
|
|
1061
|
+
* When devtools is enabled, this injects a script tag that loads the
|
|
1062
|
+
* virtual module for custom tab registration. Using a virtual module
|
|
1063
|
+
* (instead of an inline script) ensures that bare import specifiers
|
|
1064
|
+
* like `@vue/devtools-api` are resolved through Vite's module pipeline.
|
|
1065
|
+
*/
|
|
1066
|
+
transformIndexHtml() {
|
|
1067
|
+
if (!resolvedOptions.devtools || !resolvedOptions.enabled) {
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
return [
|
|
1071
|
+
{
|
|
1072
|
+
tag: "script",
|
|
1073
|
+
attrs: { type: "module", src: `/@id/${VIRTUAL_DEVTOOLS_TAB_ID}` },
|
|
1074
|
+
injectTo: "head"
|
|
1075
|
+
}
|
|
1076
|
+
];
|
|
1077
|
+
},
|
|
1078
|
+
/**
|
|
1079
|
+
* Clean up when Vite server closes
|
|
1080
|
+
*
|
|
1081
|
+
* NOTE: closeBundle() is called by Vite when the dev server shuts down
|
|
1082
|
+
* (e.g., Ctrl+C). This is the same lifecycle hook used in v0.x.
|
|
1083
|
+
* While configureServer's viteServer.httpServer?.on('close', ...) is
|
|
1084
|
+
* an alternative, closeBundle() is more reliable across Vite versions
|
|
1085
|
+
* and is the established pattern in this codebase.
|
|
1086
|
+
*/
|
|
1087
|
+
async closeBundle() {
|
|
1088
|
+
await teardown();
|
|
1089
|
+
}
|
|
1090
|
+
};
|
|
1091
|
+
async function teardown() {
|
|
1092
|
+
await Promise.allSettled(fileWatchers.map((w) => w.close()));
|
|
1093
|
+
fileWatchers = [];
|
|
1094
|
+
if (orchestrator) {
|
|
1095
|
+
try {
|
|
1096
|
+
await orchestrator.stop();
|
|
1097
|
+
} catch {
|
|
1098
|
+
}
|
|
1099
|
+
orchestrator = null;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
async function setupPerSpecFileWatching(orch, viteServer, projectCwd) {
|
|
1103
|
+
const watchers = [];
|
|
1104
|
+
try {
|
|
1105
|
+
for (const instance of orch.instances) {
|
|
1106
|
+
const specServer = instance.server;
|
|
1107
|
+
const specConfig = instance.config;
|
|
1108
|
+
const debouncedHandlerReload = debounce(
|
|
1109
|
+
() => reloadSpecHandlers(specServer, specConfig.handlersDir, viteServer, projectCwd),
|
|
1110
|
+
100
|
|
1111
|
+
);
|
|
1112
|
+
const debouncedSeedReload = debounce(
|
|
1113
|
+
() => reloadSpecSeeds(specServer, specConfig.seedsDir, viteServer, projectCwd),
|
|
1114
|
+
100
|
|
1115
|
+
);
|
|
1116
|
+
const watcher = await createFileWatcher({
|
|
1117
|
+
handlersDir: specConfig.handlersDir,
|
|
1118
|
+
seedsDir: specConfig.seedsDir,
|
|
1119
|
+
cwd: projectCwd,
|
|
1120
|
+
onHandlerChange: debouncedHandlerReload,
|
|
1121
|
+
onSeedChange: debouncedSeedReload
|
|
1122
|
+
});
|
|
1123
|
+
watchers.push(watcher);
|
|
1124
|
+
}
|
|
1125
|
+
} catch (error) {
|
|
1126
|
+
await Promise.allSettled(watchers.map((w) => w.close()));
|
|
1127
|
+
throw error;
|
|
1128
|
+
}
|
|
1129
|
+
return watchers;
|
|
1130
|
+
}
|
|
1131
|
+
async function reloadSpecHandlers(server, handlersDir, viteServer, projectCwd) {
|
|
1132
|
+
try {
|
|
1133
|
+
const logger = resolvedOptions.logger ?? console;
|
|
1134
|
+
const handlersResult = await loadHandlers(handlersDir, viteServer, projectCwd, logger);
|
|
1135
|
+
server.updateHandlers(handlersResult.handlers);
|
|
1136
|
+
server.wsHub.broadcast({
|
|
1137
|
+
type: "handlers:updated",
|
|
1138
|
+
data: { count: handlersResult.handlers.size }
|
|
1139
|
+
});
|
|
1140
|
+
printReloadNotification("handlers", handlersResult.handlers.size, resolvedOptions);
|
|
1141
|
+
} catch (error) {
|
|
1142
|
+
printError("Failed to reload handlers", error, resolvedOptions);
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
async function reloadSpecSeeds(server, seedsDir, viteServer, projectCwd) {
|
|
1146
|
+
try {
|
|
1147
|
+
const logger = resolvedOptions.logger ?? console;
|
|
1148
|
+
const seedsResult = await loadSeeds(seedsDir, viteServer, projectCwd, logger);
|
|
1149
|
+
server.store.clearAll();
|
|
1150
|
+
if (seedsResult.seeds.size > 0) {
|
|
1151
|
+
await executeSeeds(seedsResult.seeds, server.store, server.document);
|
|
1152
|
+
}
|
|
1153
|
+
server.wsHub.broadcast({
|
|
1154
|
+
type: "seeds:updated",
|
|
1155
|
+
data: { count: seedsResult.seeds.size }
|
|
1156
|
+
});
|
|
1157
|
+
printReloadNotification("seeds", seedsResult.seeds.size, resolvedOptions);
|
|
1158
|
+
} catch (error) {
|
|
1159
|
+
printError("Failed to reload seeds", error, resolvedOptions);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1129
1164
|
// src/devtools.ts
|
|
1130
1165
|
async function registerDevTools(app, options = {}) {
|
|
1131
1166
|
const { enabled = true, label = "OpenAPI Server", port, host, protocol } = options;
|
|
@@ -1156,12 +1191,12 @@ async function registerDevTools(app, options = {}) {
|
|
|
1156
1191
|
console.warn("[OpenAPI DevTools] Failed to register with Vue DevTools:", error);
|
|
1157
1192
|
}
|
|
1158
1193
|
}
|
|
1159
|
-
function getDevToolsUrl(port =
|
|
1194
|
+
function getDevToolsUrl(port = 4e3, host, protocol) {
|
|
1160
1195
|
const actualProtocol = protocol || (typeof window !== "undefined" ? window.location.protocol.replace(":", "") : "http");
|
|
1161
1196
|
const actualHost = host || (typeof window !== "undefined" ? window.location.hostname : "localhost");
|
|
1162
1197
|
return `${actualProtocol}://${actualHost}:${port}/_devtools/`;
|
|
1163
1198
|
}
|
|
1164
1199
|
|
|
1165
|
-
export { SPEC_COLORS, ValidationError, createFileWatcher, createOrchestrator, debounce, deriveProxyPath, deriveSpecId, getDevToolsUrl, getHandlerFiles, getSeedFiles, loadHandlers, loadSeeds, normalizeProxyPath, openApiServer, registerDevTools, resolveOptions, slugify, validateSpecs, validateUniqueIds, validateUniqueProxyPaths };
|
|
1200
|
+
export { API_PROXY_PATH, DEVTOOLS_PROXY_PATH, SPEC_COLORS, ValidationError, WS_PROXY_PATH, configureMultiProxy, createFileWatcher, createOrchestrator, debounce, deriveProxyPath, deriveSpecId, getDevToolsUrl, getHandlerFiles, getSeedFiles, loadHandlers, loadSeeds, normalizeProxyPath, openApiServer, registerDevTools, resolveOptions, slugify, validateSpecs, validateUniqueIds, validateUniqueProxyPaths };
|
|
1166
1201
|
//# sourceMappingURL=index.js.map
|
|
1167
1202
|
//# sourceMappingURL=index.js.map
|