@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.js CHANGED
@@ -1,11 +1,11 @@
1
- import { existsSync } from 'fs';
2
1
  import { createRequire } from 'module';
3
- import path2, { dirname, join } from 'path';
4
- import { fileURLToPath } from 'url';
5
- import { createOpenApiServer, executeSeeds, 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/types.ts
414
- var ValidationError = class extends Error {
415
- code;
416
- constructor(code, message) {
417
- super(message);
418
- this.name = "ValidationError";
419
- this.code = code;
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
- "SPEC_NOT_FOUND",
435
- `specs[${i}]${identifier}: spec field is required and must be a non-empty string (path or URL to OpenAPI spec)`
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 = Array.isArray(options.corsOrigin) && options.corsOrigin.includes("*") ? "*" : options.corsOrigin;
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
- const specConfig = options.specs[i];
1009
- specConfig.id = resolvedConfig.id;
1010
- specConfig.proxyPath = resolvedConfig.proxyPath;
1011
- specConfig.proxyPathSource = resolvedConfig.proxyPathSource;
1012
- specConfig.handlersDir = resolvedConfig.handlersDir;
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
- let createAdaptorServer;
1069
- try {
1070
- const nodeServer = await import('@hono/node-server');
1071
- createAdaptorServer = nodeServer.createAdaptorServer;
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/multi-proxy.ts
1136
- function getProxyConfig(vite) {
1137
- if (!vite.config.server) {
1138
- return null;
1139
- }
1140
- vite.config.server.proxy ??= {};
1141
- return vite.config.server.proxy;
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
- function configureMultiProxy(vite, instances, port) {
1144
- const proxyConfig = getProxyConfig(vite);
1145
- if (!proxyConfig) {
1146
- return;
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
- const httpTarget = `http://localhost:${port}`;
1149
- for (const instance of instances) {
1150
- const prefix = instance.config.proxyPath;
1151
- proxyConfig[prefix] = {
1152
- target: httpTarget,
1153
- changeOrigin: true,
1154
- rewrite: (path4) => {
1155
- if (path4 !== prefix && !path4.startsWith(`${prefix}/`) && !path4.startsWith(`${prefix}?`))
1156
- return path4;
1157
- const rest = path4.slice(prefix.length);
1158
- if (rest === "" || rest === "/") return "/";
1159
- if (rest.startsWith("?")) return `/${rest}`;
1160
- return rest;
1161
- },
1162
- headers: { "x-spec-id": instance.id }
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 = 3e3, host, protocol) {
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