@websublime/vite-plugin-open-api-server 0.24.0-next.7 → 0.24.0-next.9

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 CHANGED
@@ -513,14 +513,11 @@ interface OrchestratorResult {
513
513
  /**
514
514
  * Shared WebSocket hub for the orchestrator.
515
515
  *
516
- * Created with `autoConnect: false` so the orchestrator controls
517
- * the `connected` event (enhanced with `specs` metadata).
518
- *
519
- * In multi-spec mode (Epic 3, Task 3.1), this hub will be replaced by
520
- * `createMultiSpecWebSocketHub()` which also wires broadcast interception
521
- * and multi-spec command routing.
522
- *
523
- * @experimental Will be replaced by `createMultiSpecWebSocketHub()` in Epic 3.
516
+ * Created via `createMultiSpecWebSocketHub()` with:
517
+ * - `autoConnect: false` to suppress default connected events
518
+ * - Enhanced `addClient()` that sends specs metadata
519
+ * - Broadcast interception that adds `specId` to all events
520
+ * - Multi-spec command handler for spec-scoped routing
524
521
  */
525
522
  wsHub: WebSocketHub;
526
523
  /** Start the shared HTTP server on the configured port */
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import { createRequire } from 'module';
2
2
  import pc from 'picocolors';
3
3
  import { existsSync } from 'fs';
4
4
  import path, { dirname, join } from 'path';
5
- import { executeSeeds, mountInternalApi, createOpenApiServer, mountDevToolsRoutes, createWebSocketHub } from '@websublime/vite-plugin-open-api-core';
5
+ import { executeSeeds, createOpenApiServer, mountDevToolsRoutes, createWebSocketHub } from '@websublime/vite-plugin-open-api-core';
6
6
  export { defineHandlers, defineSeeds } from '@websublime/vite-plugin-open-api-core';
7
7
  import fg from 'fast-glob';
8
8
  import { fileURLToPath } from 'url';
@@ -582,7 +582,336 @@ function configureMultiProxy(vite, instances, port) {
582
582
 
583
583
  // package.json
584
584
  var package_default = {
585
- version: "0.24.0-next.6"};
585
+ version: "0.24.0-next.8"};
586
+
587
+ // src/multi-internal-api.ts
588
+ var PACKAGE_VERSION = package_default.version;
589
+ function mountMultiSpecInternalApi(app, instances) {
590
+ const seen = /* @__PURE__ */ new Set();
591
+ for (const inst of instances) {
592
+ if (seen.has(inst.id)) {
593
+ throw new Error(
594
+ `[vite-plugin-open-api-server] Duplicate specId "${inst.id}" in instances array`
595
+ );
596
+ }
597
+ seen.add(inst.id);
598
+ }
599
+ const instanceMap = new Map(instances.map((i) => [i.id, i]));
600
+ app.get("/_api/specs", (c) => {
601
+ const specs = instances.map((i) => ({
602
+ id: i.id,
603
+ title: i.info.title,
604
+ version: i.info.version,
605
+ proxyPath: i.config.proxyPath,
606
+ color: i.info.color,
607
+ endpoints: i.server.registry.endpoints.size,
608
+ schemas: i.server.store.getSchemas().length,
609
+ simulations: i.server.simulationManager.count()
610
+ }));
611
+ return c.json({ specs, count: specs.length });
612
+ });
613
+ app.get("/_api/registry", (c) => {
614
+ const registries = instances.map((i) => ({
615
+ specId: i.id,
616
+ specTitle: i.info.title,
617
+ specColor: i.info.color,
618
+ endpoints: Array.from(i.server.registry.endpoints.entries()).map(([key, entry]) => ({
619
+ ...entry,
620
+ key
621
+ })),
622
+ stats: i.server.registry.stats
623
+ }));
624
+ const totalEndpoints = registries.reduce((sum, r) => sum + r.endpoints.length, 0);
625
+ return c.json({
626
+ specs: registries,
627
+ totalEndpoints,
628
+ totalSpecs: registries.length
629
+ });
630
+ });
631
+ app.get("/_api/health", (c) => {
632
+ const specs = instances.map((i) => ({
633
+ id: i.id,
634
+ endpoints: i.server.registry.endpoints.size,
635
+ schemas: i.server.store.getSchemas().length,
636
+ simulations: i.server.simulationManager.count()
637
+ }));
638
+ return c.json({
639
+ status: "ok",
640
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
641
+ version: PACKAGE_VERSION,
642
+ totalSpecs: instances.length,
643
+ totalEndpoints: specs.reduce((s, i) => s + i.endpoints, 0),
644
+ specs
645
+ });
646
+ });
647
+ const specApi = new Hono();
648
+ specApi.use("/:specId/*", async (c, next) => {
649
+ const specId = c.req.param("specId");
650
+ const instance = instanceMap.get(specId);
651
+ if (!instance) {
652
+ return c.json({ error: `Unknown spec: ${specId}` }, 404);
653
+ }
654
+ c.set("specInstance", instance);
655
+ await next();
656
+ });
657
+ specApi.get("/:specId/registry", (c) => {
658
+ const instance = c.get("specInstance");
659
+ return c.json({
660
+ specId: instance.id,
661
+ endpoints: Array.from(instance.server.registry.endpoints.entries()).map(([key, entry]) => ({
662
+ ...entry,
663
+ key
664
+ })),
665
+ stats: instance.server.registry.stats
666
+ });
667
+ });
668
+ specApi.get("/:specId/store", (c) => {
669
+ const instance = c.get("specInstance");
670
+ const schemas = instance.server.store.getSchemas().map((schema) => ({
671
+ name: schema,
672
+ count: instance.server.store.getCount(schema),
673
+ idField: instance.server.store.getIdField(schema)
674
+ }));
675
+ return c.json({ specId: instance.id, schemas });
676
+ });
677
+ specApi.get("/:specId/store/:schema", (c) => {
678
+ const instance = c.get("specInstance");
679
+ const schema = c.req.param("schema");
680
+ const allItems = instance.server.store.list(schema);
681
+ const total = allItems.length;
682
+ const rawOffset = Number(c.req.query("offset"));
683
+ const offset = Number.isFinite(rawOffset) ? Math.max(Math.floor(rawOffset), 0) : 0;
684
+ const rawLimit = Number(c.req.query("limit"));
685
+ const limit = Number.isFinite(rawLimit) ? Math.min(Math.max(Math.floor(rawLimit), 0), 1e3) : Math.min(total, 1e3);
686
+ const items = limit === 0 ? [] : allItems.slice(offset, offset + limit);
687
+ return c.json({
688
+ specId: instance.id,
689
+ schema,
690
+ idField: instance.server.store.getIdField(schema),
691
+ items,
692
+ count: items.length,
693
+ total,
694
+ offset,
695
+ limit
696
+ });
697
+ });
698
+ specApi.get("/:specId/document", (c) => {
699
+ const instance = c.get("specInstance");
700
+ return c.json(instance.server.document);
701
+ });
702
+ specApi.get("/:specId/simulations", (c) => {
703
+ const instance = c.get("specInstance");
704
+ return c.json({
705
+ specId: instance.id,
706
+ simulations: instance.server.simulationManager.list(),
707
+ count: instance.server.simulationManager.count()
708
+ });
709
+ });
710
+ specApi.get("/:specId/timeline", (c) => {
711
+ const instance = c.get("specInstance");
712
+ const parsed = Number(c.req.query("limit"));
713
+ const limit = Number.isFinite(parsed) ? Math.min(Math.max(Math.floor(parsed), 0), 1e3) : 100;
714
+ const timeline = instance.server.getTimeline();
715
+ const entries = limit === 0 ? [] : timeline.slice(-limit);
716
+ return c.json({
717
+ specId: instance.id,
718
+ entries,
719
+ count: entries.length,
720
+ total: timeline.length,
721
+ limit
722
+ });
723
+ });
724
+ app.route("/_api/specs", specApi);
725
+ }
726
+
727
+ // src/multi-command.ts
728
+ var SPEC_SCOPED_COMMANDS = /* @__PURE__ */ new Set([
729
+ "get:store",
730
+ "set:store",
731
+ "clear:store",
732
+ "set:simulation",
733
+ "clear:simulation",
734
+ "reseed"
735
+ ]);
736
+ function sendError(hub, client, command, message) {
737
+ hub.sendTo(client, { type: "error", data: { command, message } });
738
+ }
739
+ function resolveInstance(specId, instanceMap, hub, client, commandType) {
740
+ if (!specId) return void 0;
741
+ const instance = instanceMap.get(specId);
742
+ if (!instance) {
743
+ sendError(hub, client, commandType, `Unknown spec: ${specId}`);
744
+ }
745
+ return instance;
746
+ }
747
+ function handleGetSpecs(hub, client, specsInfo, serverVersion) {
748
+ hub.sendTo(client, { type: "connected", data: { serverVersion, specs: specsInfo } });
749
+ }
750
+ function handleGetRegistry(cmd, hub, client, instances, instanceMap) {
751
+ const sendRegistry = (instance, id) => {
752
+ const registryEvent = {
753
+ type: "registry",
754
+ data: {
755
+ specId: id,
756
+ endpoints: Array.from(instance.server.registry.endpoints.entries()).map(([key, entry]) => ({
757
+ ...entry,
758
+ key
759
+ })),
760
+ stats: { ...instance.server.registry.stats }
761
+ }
762
+ };
763
+ hub.sendTo(client, registryEvent);
764
+ };
765
+ const specId = cmd.data?.specId;
766
+ if (specId) {
767
+ const instance = resolveInstance(specId, instanceMap, hub, client, cmd.type);
768
+ if (!instance) return;
769
+ sendRegistry(instance, specId);
770
+ } else {
771
+ for (const instance of instances) {
772
+ sendRegistry(instance, instance.id);
773
+ }
774
+ }
775
+ }
776
+ function handleGetTimeline(cmd, hub, client, instances, instanceMap) {
777
+ const specId = cmd.data?.specId;
778
+ const rawLimit = Number(cmd.data?.limit);
779
+ const limit = Number.isFinite(rawLimit) ? Math.min(Math.max(Math.floor(rawLimit), 0), 1e3) : 100;
780
+ const sendTimeline = (instance, id) => {
781
+ const timeline = instance.server.getTimeline();
782
+ const entries = limit === 0 ? [] : timeline.slice(-limit);
783
+ const timelineEvent = {
784
+ type: "timeline",
785
+ data: {
786
+ specId: id,
787
+ entries,
788
+ count: entries.length,
789
+ total: timeline.length
790
+ }
791
+ };
792
+ hub.sendTo(client, timelineEvent);
793
+ };
794
+ if (specId) {
795
+ const instance = resolveInstance(specId, instanceMap, hub, client, cmd.type);
796
+ if (!instance) return;
797
+ sendTimeline(instance, specId);
798
+ } else {
799
+ for (const instance of instances) {
800
+ sendTimeline(instance, instance.id);
801
+ }
802
+ }
803
+ }
804
+ function handleClearTimeline(cmd, hub, client, instances, instanceMap) {
805
+ const specId = cmd.data?.specId;
806
+ const clearAndNotify = (instance, id) => {
807
+ const count = instance.server.clearTimeline();
808
+ hub.sendTo(client, { type: "timeline:cleared", data: { specId: id, count } });
809
+ };
810
+ if (specId) {
811
+ const instance = resolveInstance(specId, instanceMap, hub, client, cmd.type);
812
+ if (!instance) return;
813
+ clearAndNotify(instance, specId);
814
+ } else {
815
+ for (const instance of instances) {
816
+ clearAndNotify(instance, instance.id);
817
+ }
818
+ }
819
+ }
820
+ function handleSpecScoped(cmd, hub, client, instanceMap) {
821
+ const specId = cmd.data?.specId;
822
+ if (!specId) {
823
+ sendError(hub, client, cmd.type, "specId is required for this command");
824
+ return;
825
+ }
826
+ const instance = resolveInstance(specId, instanceMap, hub, client, cmd.type);
827
+ if (!instance) return;
828
+ const { specId: _, ...coreData } = cmd.data ?? {};
829
+ const coreCommand = Object.keys(coreData).length > 0 ? { type: cmd.type, data: coreData } : { type: cmd.type };
830
+ instance.server.wsHub.handleMessage(client, JSON.stringify(coreCommand));
831
+ }
832
+ function createMultiSpecCommandHandler(deps) {
833
+ const { hub, instances, specsInfo, serverVersion } = deps;
834
+ const instanceMap = new Map(instances.map((i) => [i.id, i]));
835
+ return (client, command) => {
836
+ if (!command || typeof command !== "object" || !("type" in command)) {
837
+ return;
838
+ }
839
+ const cmd = command;
840
+ if (typeof cmd.type !== "string") {
841
+ return;
842
+ }
843
+ switch (cmd.type) {
844
+ case "get:specs":
845
+ handleGetSpecs(hub, client, specsInfo, serverVersion);
846
+ break;
847
+ case "get:registry":
848
+ handleGetRegistry(cmd, hub, client, instances, instanceMap);
849
+ break;
850
+ case "get:timeline":
851
+ handleGetTimeline(cmd, hub, client, instances, instanceMap);
852
+ break;
853
+ case "clear:timeline":
854
+ handleClearTimeline(cmd, hub, client, instances, instanceMap);
855
+ break;
856
+ default:
857
+ if (SPEC_SCOPED_COMMANDS.has(cmd.type)) {
858
+ handleSpecScoped(cmd, hub, client, instanceMap);
859
+ } else {
860
+ sendError(hub, client, cmd.type, `Unknown command type: ${cmd.type}`);
861
+ }
862
+ break;
863
+ }
864
+ };
865
+ }
866
+
867
+ // src/multi-ws.ts
868
+ var PACKAGE_VERSION2 = package_default.version;
869
+ var MULTI_SPEC_ONLY_COMMANDS = /* @__PURE__ */ new Set(["get:specs"]);
870
+ function createMultiSpecWebSocketHub(instances, specsInfo) {
871
+ const hub = createWebSocketHub({ autoConnect: false });
872
+ const originalAddClient = hub.addClient.bind(hub);
873
+ hub.addClient = (ws) => {
874
+ originalAddClient(ws);
875
+ hub.sendTo(ws, {
876
+ type: "connected",
877
+ // biome-ignore lint/suspicious/noExplicitAny: MultiSpecServerEvent extends ServerEvent with specs[]
878
+ data: { serverVersion: PACKAGE_VERSION2, specs: specsInfo }
879
+ });
880
+ };
881
+ for (const instance of instances) {
882
+ instance.server.wsHub.broadcast = (event) => {
883
+ const enriched = { type: event.type, data: { ...event.data, specId: instance.id } };
884
+ hub.broadcast(enriched);
885
+ };
886
+ instance.server.wsHub.sendTo = (client, event) => {
887
+ const enriched = { type: event.type, data: { ...event.data, specId: instance.id } };
888
+ return hub.sendTo(client, enriched);
889
+ };
890
+ }
891
+ const commandHandler = createMultiSpecCommandHandler({
892
+ hub,
893
+ instances,
894
+ specsInfo,
895
+ serverVersion: PACKAGE_VERSION2
896
+ });
897
+ hub.setCommandHandler(commandHandler);
898
+ const originalHandleMessage = hub.handleMessage.bind(hub);
899
+ hub.handleMessage = (client, message) => {
900
+ try {
901
+ const parsed = typeof message === "string" ? JSON.parse(message) : message;
902
+ if (parsed && typeof parsed === "object" && "type" in parsed) {
903
+ const cmd = parsed;
904
+ if (MULTI_SPEC_ONLY_COMMANDS.has(cmd.type)) {
905
+ commandHandler(client, cmd);
906
+ return;
907
+ }
908
+ }
909
+ } catch {
910
+ }
911
+ originalHandleMessage(client, message);
912
+ };
913
+ return hub;
914
+ }
586
915
 
587
916
  // src/types.ts
588
917
  var ValidationError = class extends Error {
@@ -821,7 +1150,6 @@ function validateUniqueIds(ids) {
821
1150
  }
822
1151
 
823
1152
  // src/orchestrator.ts
824
- var PACKAGE_VERSION = package_default.version;
825
1153
  var SPEC_COLORS = [
826
1154
  "#4ade80",
827
1155
  // green
@@ -841,16 +1169,11 @@ var SPEC_COLORS = [
841
1169
  // red
842
1170
  ];
843
1171
  async function processSpec(specConfig, index, options, vite, cwd, logger) {
844
- const specNamespace = specConfig.id ? slugify(specConfig.id) : `spec-${index}`;
845
- const handlersDir = specConfig.handlersDir || `./mocks/${specNamespace}/handlers`;
846
- const seedsDir = specConfig.seedsDir || `./mocks/${specNamespace}/seeds`;
847
- const handlersResult = await loadHandlers(handlersDir, vite, cwd, logger);
848
- const seedsResult = await loadSeeds(seedsDir, vite, cwd, logger);
849
1172
  const server = await createOpenApiServer({
850
1173
  spec: specConfig.spec,
851
1174
  port: options.port,
852
1175
  idFields: specConfig.idFields,
853
- handlers: handlersResult.handlers,
1176
+ handlers: /* @__PURE__ */ new Map(),
854
1177
  seeds: /* @__PURE__ */ new Map(),
855
1178
  timelineLimit: options.timelineLimit,
856
1179
  cors: false,
@@ -859,10 +1182,17 @@ async function processSpec(specConfig, index, options, vite, cwd, logger) {
859
1182
  // DevTools mounted at main app level
860
1183
  logger
861
1184
  });
1185
+ const id = deriveSpecId(specConfig.id, server.document);
1186
+ const handlersDir = specConfig.handlersDir || `./mocks/${id}/handlers`;
1187
+ const seedsDir = specConfig.seedsDir || `./mocks/${id}/seeds`;
1188
+ const handlersResult = await loadHandlers(handlersDir, vite, cwd, logger);
1189
+ const seedsResult = await loadSeeds(seedsDir, vite, cwd, logger);
1190
+ if (handlersResult.handlers.size > 0) {
1191
+ server.updateHandlers(handlersResult.handlers, { silent: true });
1192
+ }
862
1193
  if (seedsResult.seeds.size > 0) {
863
1194
  await executeSeeds(seedsResult.seeds, server.store, server.document);
864
1195
  }
865
- const id = deriveSpecId(specConfig.id, server.document);
866
1196
  const { proxyPath, proxyPathSource } = deriveProxyPath(specConfig.proxyPath, server.document, id);
867
1197
  const info = {
868
1198
  id,
@@ -906,25 +1236,6 @@ function mountDevToolsSpa(mainApp, logger) {
906
1236
  logger
907
1237
  });
908
1238
  }
909
- function createOrchestratorHub(specsInfo) {
910
- const wsHub = createWebSocketHub({ autoConnect: false });
911
- const descriptor = Object.getOwnPropertyDescriptor(wsHub, "addClient");
912
- if (descriptor && !descriptor.writable && !descriptor.configurable) {
913
- throw new Error(
914
- "[vite-plugin-open-api-server] Cannot override wsHub.addClient: property is non-writable and non-configurable. The core WebSocketHub implementation may have changed. This will be resolved by createMultiSpecWebSocketHub() in Epic 3."
915
- );
916
- }
917
- const originalAddClient = wsHub.addClient.bind(wsHub);
918
- wsHub.addClient = (ws) => {
919
- originalAddClient(ws);
920
- wsHub.sendTo(ws, {
921
- type: "connected",
922
- // biome-ignore lint/suspicious/noExplicitAny: MultiSpecServerEvent connected data extends ServerEvent connected data with specs[]
923
- data: { serverVersion: PACKAGE_VERSION, specs: specsInfo }
924
- });
925
- };
926
- return wsHub;
927
- }
928
1239
  async function mountWebSocketRoute(mainApp, wsHub, logger) {
929
1240
  let nodeWsModule;
930
1241
  try {
@@ -1022,30 +1333,11 @@ async function createOrchestrator(options, vite, cwd) {
1022
1333
  if (options.devtools) {
1023
1334
  mountDevToolsSpa(mainApp, logger);
1024
1335
  }
1025
- if (instances.length > 1) {
1026
- logger.warn?.(
1027
- "[vite-plugin-open-api-server] Only first spec's internal API mounted on /_api; multi-spec support planned in Epic 3 (Task 3.x)."
1028
- );
1029
- mainApp.use("/_api/*", async (c, next) => {
1030
- await next();
1031
- c.header("X-Multi-Spec-Warning", `Only showing data for spec "${instances[0].id}"`);
1032
- });
1033
- }
1034
1336
  if (instances.length > 0) {
1035
- const firstInstance = instances[0];
1036
- mountInternalApi(mainApp, {
1037
- store: firstInstance.server.store,
1038
- registry: firstInstance.server.registry,
1039
- simulationManager: firstInstance.server.simulationManager,
1040
- wsHub: firstInstance.server.wsHub,
1041
- timeline: firstInstance.server.getTimeline(),
1042
- timelineLimit: options.timelineLimit,
1043
- clearTimeline: () => firstInstance.server.truncateTimeline(),
1044
- document: firstInstance.server.document
1045
- });
1337
+ mountMultiSpecInternalApi(mainApp, instances);
1046
1338
  }
1047
1339
  const specsInfo = instances.map((inst) => inst.info);
1048
- const wsHub = createOrchestratorHub(specsInfo);
1340
+ const wsHub = createMultiSpecWebSocketHub(instances, specsInfo);
1049
1341
  const { injectWebSocket } = await mountWebSocketRoute(mainApp, wsHub, logger);
1050
1342
  const instanceMap = new Map(instances.map((inst) => [inst.id, inst]));
1051
1343
  mainApp.use("*", createDispatchMiddleware(instanceMap));