@websublime/vite-plugin-open-api-server 0.24.0-next.4 → 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 +60 -11
- package/dist/index.js +548 -564
- 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,343 +645,20 @@ async function getSeedFiles(seedsDir, cwd = process.cwd()) {
|
|
|
410
645
|
return files;
|
|
411
646
|
}
|
|
412
647
|
|
|
413
|
-
// src/
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
};
|
|
422
|
-
function validateSpecs(specs) {
|
|
423
|
-
if (!specs || !Array.isArray(specs) || specs.length === 0) {
|
|
424
|
-
throw new ValidationError(
|
|
425
|
-
"SPECS_EMPTY",
|
|
426
|
-
"specs is required and must be a non-empty array of SpecConfig"
|
|
427
|
-
);
|
|
428
|
-
}
|
|
429
|
-
for (let i = 0; i < specs.length; i++) {
|
|
430
|
-
const spec = specs[i];
|
|
431
|
-
if (!spec.spec || typeof spec.spec !== "string" || spec.spec.trim() === "") {
|
|
432
|
-
const identifier = spec.id ? ` (id: "${spec.id}")` : "";
|
|
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) {
|
|
433
656
|
throw new ValidationError(
|
|
434
|
-
"
|
|
435
|
-
`
|
|
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.`
|
|
436
659
|
);
|
|
437
660
|
}
|
|
438
|
-
|
|
439
|
-
}
|
|
440
|
-
function resolveOptions(options) {
|
|
441
|
-
validateSpecs(options.specs);
|
|
442
|
-
return {
|
|
443
|
-
specs: options.specs.map((s) => ({
|
|
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;
|
|
661
|
+
return id2;
|
|
750
662
|
}
|
|
751
663
|
const title = document.info?.title;
|
|
752
664
|
if (!title || !title.trim()) {
|
|
@@ -755,141 +667,34 @@ function deriveSpecId(explicitId, document) {
|
|
|
755
667
|
"Cannot derive spec ID: info.title is missing from the OpenAPI document. Please set an explicit id in the spec configuration."
|
|
756
668
|
);
|
|
757
669
|
}
|
|
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
|
-
}
|
|
884
|
-
if (longer.startsWith(shorter)) {
|
|
885
|
-
throw new ValidationError(
|
|
886
|
-
"PROXY_PATH_PREFIX_COLLISION",
|
|
887
|
-
`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}".`
|
|
888
|
-
);
|
|
889
|
-
}
|
|
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);
|
|
890
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
|
+
);
|
|
891
694
|
}
|
|
892
695
|
}
|
|
696
|
+
|
|
697
|
+
// src/orchestrator.ts
|
|
893
698
|
var SPEC_COLORS = [
|
|
894
699
|
"#4ade80",
|
|
895
700
|
// green
|
|
@@ -948,7 +753,7 @@ async function processSpec(specConfig, index, options, vite, cwd, logger) {
|
|
|
948
753
|
}
|
|
949
754
|
function buildCorsConfig(options) {
|
|
950
755
|
const isWildcardOrigin = options.corsOrigin === "*" || Array.isArray(options.corsOrigin) && options.corsOrigin.includes("*");
|
|
951
|
-
const effectiveCorsOrigin =
|
|
756
|
+
const effectiveCorsOrigin = isWildcardOrigin ? "*" : options.corsOrigin;
|
|
952
757
|
return {
|
|
953
758
|
origin: effectiveCorsOrigin,
|
|
954
759
|
allowMethods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"],
|
|
@@ -1005,12 +810,11 @@ async function createOrchestrator(options, vite, cwd) {
|
|
|
1005
810
|
cwd,
|
|
1006
811
|
logger
|
|
1007
812
|
);
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
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;
|
|
1014
818
|
instances.push(instance);
|
|
1015
819
|
}
|
|
1016
820
|
validateUniqueIds(instances.map((inst) => inst.id));
|
|
@@ -1031,12 +835,16 @@ async function createOrchestrator(options, vite, cwd) {
|
|
|
1031
835
|
if (options.devtools) {
|
|
1032
836
|
mountDevToolsSpa(mainApp, logger);
|
|
1033
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
|
+
}
|
|
1034
847
|
if (instances.length > 0) {
|
|
1035
|
-
if (instances.length > 1) {
|
|
1036
|
-
logger.warn?.(
|
|
1037
|
-
"[vite-plugin-open-api-server] Only first spec's internal API mounted on /_api; multi-spec support planned in Epic 3 (Task 3.x)."
|
|
1038
|
-
);
|
|
1039
|
-
}
|
|
1040
848
|
const firstInstance = instances[0];
|
|
1041
849
|
mountInternalApi(mainApp, {
|
|
1042
850
|
store: firstInstance.server.store,
|
|
@@ -1054,6 +862,37 @@ async function createOrchestrator(options, vite, cwd) {
|
|
|
1054
862
|
const specsInfo = instances.map((inst) => inst.info);
|
|
1055
863
|
let serverInstance = null;
|
|
1056
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
|
+
}
|
|
1057
896
|
return {
|
|
1058
897
|
app: mainApp,
|
|
1059
898
|
instances,
|
|
@@ -1065,51 +904,18 @@ async function createOrchestrator(options, vite, cwd) {
|
|
|
1065
904
|
if (serverInstance) {
|
|
1066
905
|
throw new Error("[vite-plugin-open-api-server] Server already running. Call stop() first.");
|
|
1067
906
|
}
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
} catch {
|
|
1073
|
-
throw new Error(
|
|
1074
|
-
"@hono/node-server is required. Install with: npm install @hono/node-server"
|
|
1075
|
-
);
|
|
1076
|
-
}
|
|
1077
|
-
const server = createAdaptorServer({ fetch: mainApp.fetch });
|
|
1078
|
-
await new Promise((resolve, reject) => {
|
|
1079
|
-
const onListening = () => {
|
|
1080
|
-
server.removeListener("error", onError);
|
|
1081
|
-
const addr = server.address();
|
|
1082
|
-
const actualPort = typeof addr === "object" && addr ? addr.port : options.port;
|
|
1083
|
-
logger.info(
|
|
1084
|
-
`[vite-plugin-open-api-server] Server started on http://localhost:${actualPort}`
|
|
1085
|
-
);
|
|
1086
|
-
boundPort = actualPort;
|
|
1087
|
-
serverInstance = server;
|
|
1088
|
-
resolve();
|
|
1089
|
-
};
|
|
1090
|
-
const onError = (err) => {
|
|
1091
|
-
server.removeListener("listening", onListening);
|
|
1092
|
-
server.close(() => {
|
|
1093
|
-
});
|
|
1094
|
-
serverInstance = null;
|
|
1095
|
-
boundPort = 0;
|
|
1096
|
-
if (err.code === "EADDRINUSE") {
|
|
1097
|
-
reject(
|
|
1098
|
-
new Error(`[vite-plugin-open-api-server] Port ${options.port} is already in use.`)
|
|
1099
|
-
);
|
|
1100
|
-
} else {
|
|
1101
|
-
reject(new Error(`[vite-plugin-open-api-server] Server error: ${err.message}`));
|
|
1102
|
-
}
|
|
1103
|
-
};
|
|
1104
|
-
server.once("listening", onListening);
|
|
1105
|
-
server.once("error", onError);
|
|
1106
|
-
server.listen(options.port);
|
|
1107
|
-
});
|
|
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}`);
|
|
1108
911
|
},
|
|
1109
912
|
async stop() {
|
|
1110
913
|
const server = serverInstance;
|
|
1111
914
|
if (server) {
|
|
1112
915
|
try {
|
|
916
|
+
if (typeof server.closeAllConnections === "function") {
|
|
917
|
+
server.closeAllConnections();
|
|
918
|
+
}
|
|
1113
919
|
await new Promise((resolve, reject) => {
|
|
1114
920
|
server.close((err) => {
|
|
1115
921
|
if (err) {
|
|
@@ -1132,49 +938,227 @@ async function createOrchestrator(options, vite, cwd) {
|
|
|
1132
938
|
};
|
|
1133
939
|
}
|
|
1134
940
|
|
|
1135
|
-
// src/
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
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
|
|
1142
1010
|
}
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
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
|
+
}
|
|
1147
1101
|
}
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
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
|
+
}
|
|
1164
1161
|
}
|
|
1165
|
-
proxyConfig["/_devtools"] = {
|
|
1166
|
-
target: httpTarget,
|
|
1167
|
-
changeOrigin: true
|
|
1168
|
-
};
|
|
1169
|
-
proxyConfig["/_api"] = {
|
|
1170
|
-
target: httpTarget,
|
|
1171
|
-
changeOrigin: true
|
|
1172
|
-
};
|
|
1173
|
-
proxyConfig["/_ws"] = {
|
|
1174
|
-
target: `ws://localhost:${port}`,
|
|
1175
|
-
changeOrigin: true,
|
|
1176
|
-
ws: true
|
|
1177
|
-
};
|
|
1178
1162
|
}
|
|
1179
1163
|
|
|
1180
1164
|
// src/devtools.ts
|
|
@@ -1207,12 +1191,12 @@ async function registerDevTools(app, options = {}) {
|
|
|
1207
1191
|
console.warn("[OpenAPI DevTools] Failed to register with Vue DevTools:", error);
|
|
1208
1192
|
}
|
|
1209
1193
|
}
|
|
1210
|
-
function getDevToolsUrl(port =
|
|
1194
|
+
function getDevToolsUrl(port = 4e3, host, protocol) {
|
|
1211
1195
|
const actualProtocol = protocol || (typeof window !== "undefined" ? window.location.protocol.replace(":", "") : "http");
|
|
1212
1196
|
const actualHost = host || (typeof window !== "undefined" ? window.location.hostname : "localhost");
|
|
1213
1197
|
return `${actualProtocol}://${actualHost}:${port}/_devtools/`;
|
|
1214
1198
|
}
|
|
1215
1199
|
|
|
1216
|
-
export { SPEC_COLORS, ValidationError, configureMultiProxy, 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 };
|
|
1217
1201
|
//# sourceMappingURL=index.js.map
|
|
1218
1202
|
//# sourceMappingURL=index.js.map
|