@websublime/vite-plugin-open-api-server 0.24.0-next.8 → 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.7"};
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
@@ -908,25 +1236,6 @@ function mountDevToolsSpa(mainApp, logger) {
908
1236
  logger
909
1237
  });
910
1238
  }
911
- function createOrchestratorHub(specsInfo) {
912
- const wsHub = createWebSocketHub({ autoConnect: false });
913
- const descriptor = Object.getOwnPropertyDescriptor(wsHub, "addClient");
914
- if (descriptor && !descriptor.writable && !descriptor.configurable) {
915
- throw new Error(
916
- "[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."
917
- );
918
- }
919
- const originalAddClient = wsHub.addClient.bind(wsHub);
920
- wsHub.addClient = (ws) => {
921
- originalAddClient(ws);
922
- wsHub.sendTo(ws, {
923
- type: "connected",
924
- // biome-ignore lint/suspicious/noExplicitAny: MultiSpecServerEvent connected data extends ServerEvent connected data with specs[]
925
- data: { serverVersion: PACKAGE_VERSION, specs: specsInfo }
926
- });
927
- };
928
- return wsHub;
929
- }
930
1239
  async function mountWebSocketRoute(mainApp, wsHub, logger) {
931
1240
  let nodeWsModule;
932
1241
  try {
@@ -1024,30 +1333,11 @@ async function createOrchestrator(options, vite, cwd) {
1024
1333
  if (options.devtools) {
1025
1334
  mountDevToolsSpa(mainApp, logger);
1026
1335
  }
1027
- if (instances.length > 1) {
1028
- logger.warn?.(
1029
- "[vite-plugin-open-api-server] Only first spec's internal API mounted on /_api; multi-spec support planned in Epic 3 (Task 3.x)."
1030
- );
1031
- mainApp.use("/_api/*", async (c, next) => {
1032
- await next();
1033
- c.header("X-Multi-Spec-Warning", `Only showing data for spec "${instances[0].id}"`);
1034
- });
1035
- }
1036
1336
  if (instances.length > 0) {
1037
- const firstInstance = instances[0];
1038
- mountInternalApi(mainApp, {
1039
- store: firstInstance.server.store,
1040
- registry: firstInstance.server.registry,
1041
- simulationManager: firstInstance.server.simulationManager,
1042
- wsHub: firstInstance.server.wsHub,
1043
- timeline: firstInstance.server.getTimeline(),
1044
- timelineLimit: options.timelineLimit,
1045
- clearTimeline: () => firstInstance.server.truncateTimeline(),
1046
- document: firstInstance.server.document
1047
- });
1337
+ mountMultiSpecInternalApi(mainApp, instances);
1048
1338
  }
1049
1339
  const specsInfo = instances.map((inst) => inst.info);
1050
- const wsHub = createOrchestratorHub(specsInfo);
1340
+ const wsHub = createMultiSpecWebSocketHub(instances, specsInfo);
1051
1341
  const { injectWebSocket } = await mountWebSocketRoute(mainApp, wsHub, logger);
1052
1342
  const instanceMap = new Map(instances.map((inst) => [inst.id, inst]));
1053
1343
  mainApp.use("*", createDispatchMiddleware(instanceMap));