From 005682693dbf2d60bc9fbfa55926d80282fcdd0a Mon Sep 17 00:00:00 2001 From: Miguel Ramos Date: Tue, 3 Mar 2026 14:44:10 +0000 Subject: [PATCH 1/6] fix(core,server): implement two-phase seed population correctly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three bugs caused the seed response priority path to be dead code: 1. updateSeeds() in server.ts reassigned currentSeeds instead of using .clear()+.set() in-place mutation. Route closures captured the original Map by reference, so reassignment broke the reference chain. Now matches the updateHandlers() pattern. 2. The orchestrator's processSpec() called executeSeeds() to populate the store but never called server.updateSeeds() to sync the route builder's seed map. Added buildSeedMapFromStore() helper that reads materialized data from the store and passes it to updateSeeds(). 3. Hot reload's reloadSpecSeeds() had the same gap — executeSeeds() populated the store but the route builder seed map was never synced. Now calls updateSeeds() after executeSeeds(), which also handles registry hasSeed flags and WebSocket broadcast. Closes vite-20k --- packages/core/src/server.ts | 36 +++++-- .../src/__tests__/per-spec-reload.test.ts | 93 ++++++++++++------- packages/server/src/hot-reload.ts | 55 +++++++---- packages/server/src/orchestrator.ts | 33 ++++++- 4 files changed, 154 insertions(+), 63 deletions(-) diff --git a/packages/core/src/server.ts b/packages/core/src/server.ts index 646530e9..6d48018c 100644 --- a/packages/core/src/server.ts +++ b/packages/core/src/server.ts @@ -269,13 +269,14 @@ export async function createOpenApiServer(config: OpenApiServerConfig): Promise< } } - // Current handlers (mutable for hot reload). - // IMPORTANT: Route closures in buildRoutes capture this Map by reference. - // updateHandlers() mutates it in-place (clear + re-populate) so that existing - // route closures see the updated entries. Never reassign this variable — doing - // so would break the reference chain and silently stop handler dispatch. + // Current handlers and seeds (mutable for hot reload). + // IMPORTANT: Route closures in buildRoutes capture these Maps by reference. + // updateHandlers()/updateSeeds() mutate them in-place (clear + re-populate) so + // that existing route closures see the updated entries. Never reassign these + // variables — doing so would break the reference chain and silently stop + // handler/seed dispatch. const currentHandlers = handlers; - let currentSeeds = seeds; + const currentSeeds = seeds; // Build routes from OpenAPI document. // IMPORTANT: buildRoutes must receive the exact same Map instances stored in @@ -557,15 +558,30 @@ export async function createOpenApiServer(config: OpenApiServerConfig): Promise< /** * Update seed data at runtime (for hot reload) * + * Repopulates the store with the new seed data and syncs the route + * builder's seed map so that the seed response priority path sees + * the updated entries. + * * @remarks - * **Warning**: This method clears ALL data in the store before repopulating - * with the new seeds. Any manually added data (including data in schemas - * not present in the new seeds) will be permanently lost. + * **Mutation contract**: Route closures capture `currentSeeds` by + * reference at build time. This method mutates the Map in-place + * (`.clear()` + `.set()`) so closures see the updates. Never replace + * the Map — that breaks the reference chain. + * + * **Warning**: This method clears ALL data in the store before + * repopulating with the new seeds. Any manually added data (including + * data in schemas not present in the new seeds) will be permanently lost. * * @param newSeeds - New seeds map (schema name -> array of items) */ updateSeeds(newSeeds: Map): void { - currentSeeds = newSeeds; + // Mutate the existing Map in-place so route closures see the updates. + // IMPORTANT: buildRoutes captures this Map by reference — never replace it. + // This mirrors the pattern used by updateHandlers(). + currentSeeds.clear(); + for (const [key, value] of newSeeds) { + currentSeeds.set(key, value); + } // Re-populate store with new seeds // Note: clearAll() removes ALL schemas, not just the ones being updated diff --git a/packages/server/src/__tests__/per-spec-reload.test.ts b/packages/server/src/__tests__/per-spec-reload.test.ts index 9efeb3e8..415b343a 100644 --- a/packages/server/src/__tests__/per-spec-reload.test.ts +++ b/packages/server/src/__tests__/per-spec-reload.test.ts @@ -352,7 +352,7 @@ describe('Per-Spec Reload Isolation', () => { expect(specB.server.store.clearAll).not.toHaveBeenCalled(); }); - it('should only broadcast to the targeted spec wsHub', async () => { + it('should call updateSeeds on the targeted spec only', async () => { mockedLoadSeeds.mockResolvedValue({ seeds: new Map(), fileCount: 0, @@ -361,31 +361,37 @@ describe('Per-Spec Reload Isolation', () => { await reloadSpecSeeds(specA, mockVite, cwd, options); - // Spec A wsHub should have broadcast called - expect(specA.server.wsHub.broadcast).toHaveBeenCalledTimes(1); - expect(specA.server.wsHub.broadcast).toHaveBeenCalledWith({ - type: 'seeds:updated', - data: { count: 0 }, - }); + // Spec A should have updateSeeds called (which handles broadcast internally) + expect(specA.server.updateSeeds).toHaveBeenCalledTimes(1); + expect(specA.server.updateSeeds).toHaveBeenCalledWith(new Map()); - // Spec B wsHub should NOT have broadcast called - expect(specB.server.wsHub.broadcast).not.toHaveBeenCalled(); + // Spec B should NOT have updateSeeds called + expect(specB.server.updateSeeds).not.toHaveBeenCalled(); }); - it('should print reload notification with seeds.size (not fileCount)', async () => { + it('should print reload notification with seed schema count from store', async () => { const newSeeds = new Map([['Pet', [{ id: 1, name: 'Rex' }]]]); // fileCount deliberately differs from seeds.size to ensure - // the implementation uses seeds.size for the notification + // the implementation uses the store's schema count for the notification mockedLoadSeeds.mockResolvedValue({ seeds: newSeeds, fileCount: 3, files: ['pets.seeds.ts', 'owners.seeds.ts', 'admin.seeds.ts'], }); + // After executeSeeds, the store should have the seeded schemas + const mockStore = specA.server.store as unknown as MockStore; + mockedExecuteSeeds.mockImplementation(async () => { + // Simulate store being populated by executeSeeds + mockStore.getSchemas.mockReturnValue(['Pet']); + mockStore.list.mockReturnValue([{ id: 1, name: 'Rex' }]); + return { schemaCount: 1, totalItems: 1, itemsPerSchema: { Pet: 1 }, skippedSchemas: [], warnings: [] }; + }); + await reloadSpecSeeds(specA, mockVite, cwd, options); - // Should report seeds.size (1), not fileCount (3) + // Should report seed map size (1), not fileCount (3) expect(mockedPrintReloadNotification).toHaveBeenCalledWith('seeds', 1, options); }); @@ -398,6 +404,8 @@ describe('Per-Spec Reload Isolation', () => { await reloadSpecSeeds(specA, mockVite, cwd, options); + // updateSeeds should still be called (with empty map) to clear stale seed data + expect(specA.server.updateSeeds).toHaveBeenCalledWith(new Map()); expect(mockedPrintReloadNotification).not.toHaveBeenCalled(); }); @@ -435,11 +443,11 @@ describe('Per-Spec Reload Isolation', () => { // Verify spec B is completely untouched expect(specB.server.store.clearAll).not.toHaveBeenCalled(); - expect(specB.server.wsHub.broadcast).not.toHaveBeenCalled(); + expect(specB.server.updateSeeds).not.toHaveBeenCalled(); expect(specB.server.updateHandlers).not.toHaveBeenCalled(); }); - it('should clear store even when no seeds are loaded', async () => { + it('should clear store and sync empty seeds when no seeds are loaded', async () => { mockedLoadSeeds.mockResolvedValue({ seeds: new Map(), fileCount: 0, @@ -450,6 +458,9 @@ describe('Per-Spec Reload Isolation', () => { // Store should still be cleared (empty seeds = clear all data) expect(specA.server.store.clearAll).toHaveBeenCalledTimes(1); + + // updateSeeds should be called with empty map to clear stale seed data in route builder + expect(specA.server.updateSeeds).toHaveBeenCalledWith(new Map()); }); it('should handle errors without affecting other specs', async () => { @@ -467,12 +478,12 @@ describe('Per-Spec Reload Isolation', () => { // No reload notification on error expect(mockedPrintReloadNotification).not.toHaveBeenCalled(); - // No broadcast on seed-load failure (store was never modified) - expect(specA.server.wsHub.broadcast).not.toHaveBeenCalled(); + // No updateSeeds on seed-load failure (store was never modified) + expect(specA.server.updateSeeds).not.toHaveBeenCalled(); // Even with an error, spec B should be untouched expect(specB.server.store.clearAll).not.toHaveBeenCalled(); - expect(specB.server.wsHub.broadcast).not.toHaveBeenCalled(); + expect(specB.server.updateSeeds).not.toHaveBeenCalled(); }); it('should reload two specs independently', async () => { @@ -482,6 +493,14 @@ describe('Per-Spec Reload Isolation', () => { ['Category', [{ id: 1, name: 'Tools' }]], ]); + // Configure spec A mock store to return data after executeSeeds + const mockStoreA = specA.server.store as unknown as MockStore; + mockedExecuteSeeds.mockImplementationOnce(async () => { + mockStoreA.getSchemas.mockReturnValue(['Pet']); + mockStoreA.list.mockReturnValue([{ id: 1, name: 'Rex' }]); + return { schemaCount: 1, totalItems: 1, itemsPerSchema: { Pet: 1 }, skippedSchemas: [], warnings: [] }; + }); + // Reload spec A mockedLoadSeeds.mockResolvedValueOnce({ seeds: specASeeds, @@ -490,6 +509,18 @@ describe('Per-Spec Reload Isolation', () => { }); await reloadSpecSeeds(specA, mockVite, cwd, options); + // Configure spec B mock store to return data after executeSeeds + const mockStoreB = specB.server.store as unknown as MockStore; + mockedExecuteSeeds.mockImplementationOnce(async () => { + mockStoreB.getSchemas.mockReturnValue(['Item', 'Category']); + mockStoreB.list.mockImplementation((schema: string) => { + if (schema === 'Item') return [{ id: 1, sku: 'A001' }]; + if (schema === 'Category') return [{ id: 1, name: 'Tools' }]; + return []; + }); + return { schemaCount: 2, totalItems: 2, itemsPerSchema: { Item: 1, Category: 1 }, skippedSchemas: [], warnings: [] }; + }); + // Reload spec B mockedLoadSeeds.mockResolvedValueOnce({ seeds: specBSeeds, @@ -502,15 +533,16 @@ describe('Per-Spec Reload Isolation', () => { expect(specA.server.store.clearAll).toHaveBeenCalledTimes(1); expect(specB.server.store.clearAll).toHaveBeenCalledTimes(1); - // Each spec broadcast independently - expect(specA.server.wsHub.broadcast).toHaveBeenCalledWith({ - type: 'seeds:updated', - data: { count: 1 }, - }); - expect(specB.server.wsHub.broadcast).toHaveBeenCalledWith({ - type: 'seeds:updated', - data: { count: 2 }, - }); + // Each spec updateSeeds called independently with data from store + expect(specA.server.updateSeeds).toHaveBeenCalledWith( + new Map([['Pet', [{ id: 1, name: 'Rex' }]]]), + ); + expect(specB.server.updateSeeds).toHaveBeenCalledWith( + new Map([ + ['Item', [{ id: 1, sku: 'A001' }]], + ['Category', [{ id: 1, name: 'Tools' }]], + ]), + ); }); }); @@ -576,11 +608,8 @@ describe('Per-Spec Reload Isolation', () => { options, ); - // Should broadcast with count 0 to reflect the cleared state - expect(specA.server.wsHub.broadcast).toHaveBeenCalledWith({ - type: 'seeds:updated', - data: { count: 0 }, - }); + // updateSeeds should be called with empty map to clear stale seed data + expect(specA.server.updateSeeds).toHaveBeenCalledWith(new Map()); // No reload notification on executeSeeds failure expect(mockedPrintReloadNotification).not.toHaveBeenCalled(); @@ -598,7 +627,7 @@ describe('Per-Spec Reload Isolation', () => { // Spec B should be completely untouched expect(specB.server.store.clearAll).not.toHaveBeenCalled(); - expect(specB.server.wsHub.broadcast).not.toHaveBeenCalled(); + expect(specB.server.updateSeeds).not.toHaveBeenCalled(); }); }); }); diff --git a/packages/server/src/hot-reload.ts b/packages/server/src/hot-reload.ts index 556702a7..e5c5d53e 100644 --- a/packages/server/src/hot-reload.ts +++ b/packages/server/src/hot-reload.ts @@ -11,7 +11,7 @@ import { existsSync } from 'node:fs'; import path from 'node:path'; -import { executeSeeds, type Logger } from '@websublime/vite-plugin-open-api-core'; +import { executeSeeds, type Logger, type Store } from '@websublime/vite-plugin-open-api-core'; import type { FSWatcher } from 'chokidar'; import type { ViteDevServer } from 'vite'; import { printError, printReloadNotification } from './banner.js'; @@ -457,11 +457,32 @@ export async function reloadSpecHandlers( } } +/** + * Build a seed data Map from the store's current contents. + * + * After `executeSeeds()` populates the store, this reads back the + * materialized data so it can be passed to `server.updateSeeds()`. + * + * @param store - Store populated by executeSeeds() + * @returns Map of schema name to array of items + */ +function buildSeedMapFromStore(store: Store): Map { + const seedMap = new Map(); + for (const schemaName of store.getSchemas()) { + const items = store.list(schemaName); + if (items.length > 0) { + seedMap.set(schemaName, items); + } + } + return seedMap; +} + /** * Reload seeds for a specific spec instance * - * Loads fresh seeds from disk, clears the spec's store, and re-executes - * seeds. Broadcasts a WebSocket event and logs the result. + * Loads fresh seeds from disk, clears the spec's store, re-executes + * seeds, and syncs the route builder's seed map via `updateSeeds()`. + * Broadcasts a WebSocket event and logs the result. * * Note: This operation is not fully atomic — there's a brief window between * clearing the store and repopulating it where requests may see empty data. @@ -480,41 +501,35 @@ export async function reloadSpecSeeds( ): Promise { try { // Load seeds first (before clearing) to minimize the window where store is empty. - // NOTE: We bypass instance.server.updateSeeds() because it expects static data - // (Map), while loadSeeds() returns seed functions (Map) - // that must be materialized via executeSeeds(). The explicit broadcast below is necessary - // because we're not going through updateSeeds()'s built-in broadcast. - // TODO: Epic 3 (Task 3.1) will wire the broadcast wrapper to add specId automatically. - // TODO: Epic 3 (Task 3.2) should also update registry hasSeed flags after seed reload, - // which are currently skipped because updateSeeds() is bypassed. const logger = options.logger ?? console; const seedsResult = await loadSeeds(instance.config.seedsDir, vite, cwd, logger); - let broadcastCount = seedsResult.seeds.size; instance.server.store.clearAll(); if (seedsResult.seeds.size > 0) { try { await executeSeeds(seedsResult.seeds, instance.server.store, instance.server.document); } catch (execError) { - // Store was already cleared — warn that it's now empty due to seed execution failure - broadcastCount = 0; + // Store was already cleared — warn that it's now empty due to seed execution failure. + // Sync an empty seed map so the route builder doesn't serve stale data. + instance.server.updateSeeds(new Map()); printError( `Seeds loaded but executeSeeds failed for spec "${instance.id}"; store is now empty`, execError, options, ); + return; } } - // Single broadcast for both success and failure paths - instance.server.wsHub.broadcast({ - type: 'seeds:updated', - data: { count: broadcastCount }, - }); + // Sync the route builder's seed map from the now-populated store. + // updateSeeds() handles: in-place map mutation, registry hasSeed flags, + // WebSocket broadcast, and logging. + const seedMap = buildSeedMapFromStore(instance.server.store); + instance.server.updateSeeds(seedMap); - if (broadcastCount > 0) { - printReloadNotification('seeds', broadcastCount, options); + if (seedMap.size > 0) { + printReloadNotification('seeds', seedMap.size, options); } } catch (error) { printError(`Failed to reload seeds for spec "${instance.id}"`, error, options); diff --git a/packages/server/src/orchestrator.ts b/packages/server/src/orchestrator.ts index 3207d205..0063e27d 100644 --- a/packages/server/src/orchestrator.ts +++ b/packages/server/src/orchestrator.ts @@ -19,6 +19,7 @@ import { mountDevToolsRoutes, type OpenApiServer, type SpecInfo, + type Store, type WebSocketHub, } from '@websublime/vite-plugin-open-api-core'; import { type Context, Hono } from 'hono'; @@ -142,6 +143,29 @@ interface ProcessedSpec { }; } +/** + * Build a seed data Map from the store's current contents. + * + * After `executeSeeds()` populates the store, this function reads back + * the materialized data so it can be passed to `server.updateSeeds()`. + * The route builder's seed map needs static `Map` + * data (not seed functions), which is exactly what the store contains + * after execution. + * + * @param store - Store populated by executeSeeds() + * @returns Map of schema name to array of items + */ +function buildSeedMapFromStore(store: Store): Map { + const seedMap = new Map(); + for (const schemaName of store.getSchemas()) { + const items = store.list(schemaName); + if (items.length > 0) { + seedMap.set(schemaName, items); + } + } + return seedMap; +} + /** * Process a single spec configuration into a resolved SpecInstance. * @@ -191,9 +215,16 @@ async function processSpec( server.updateHandlers(handlersResult.handlers, { silent: true }); } - // Execute seed functions to populate the store + // Execute seed functions to populate the store, then sync route builder if (seedsResult.seeds.size > 0) { await executeSeeds(seedsResult.seeds, server.store, server.document); + + // Sync the route builder's seed map from the now-populated store. + // executeSeeds() only writes to the store — the route builder's seed map + // (used for the seed response priority path) is a separate reference that + // must be updated explicitly via updateSeeds(). + const seedMap = buildSeedMapFromStore(server.store); + server.updateSeeds(seedMap); } // Derive proxy path (from explicit config or servers[0].url) From 7112339a6d1364052507d94381ea1cc08a8b2537 Mon Sep 17 00:00:00 2001 From: Miguel Ramos Date: Tue, 3 Mar 2026 14:44:46 +0000 Subject: [PATCH 2/6] chore: add changeset for two-phase seed population fix --- .changesets/fix-vite-20k-seed-population.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .changesets/fix-vite-20k-seed-population.json diff --git a/.changesets/fix-vite-20k-seed-population.json b/.changesets/fix-vite-20k-seed-population.json new file mode 100644 index 00000000..ea6cff1c --- /dev/null +++ b/.changesets/fix-vite-20k-seed-population.json @@ -0,0 +1,14 @@ +{ + "branch": "fix/vite-20k-seed-population", + "bump": "patch", + "environments": [ + "production" + ], + "packages": [ + "@websublime/vite-plugin-open-api-core", + "@websublime/vite-plugin-open-api-server" + ], + "changes": [], + "created_at": "2026-03-03T14:44:38.222145Z", + "updated_at": "2026-03-03T14:44:38.222672Z" +} \ No newline at end of file From b8779696846cb63729187cb4c5fcd3553eb7460f Mon Sep 17 00:00:00 2001 From: Miguel Ramos Date: Tue, 3 Mar 2026 14:45:03 +0000 Subject: [PATCH 3/6] chore: sync changeset for fix/vite-20k-seed-population --- .changesets/fix-vite-20k-seed-population.json | 117 +++++++++++++++++- 1 file changed, 115 insertions(+), 2 deletions(-) diff --git a/.changesets/fix-vite-20k-seed-population.json b/.changesets/fix-vite-20k-seed-population.json index ea6cff1c..e13b366a 100644 --- a/.changesets/fix-vite-20k-seed-population.json +++ b/.changesets/fix-vite-20k-seed-population.json @@ -8,7 +8,120 @@ "@websublime/vite-plugin-open-api-core", "@websublime/vite-plugin-open-api-server" ], - "changes": [], + "changes": [ + "7112339a6d1364052507d94381ea1cc08a8b2537", + "005682693dbf2d60bc9fbfa55926d80282fcdd0a", + "f345067c1cfbbce92a17a61f210ae2aebabec801", + "f317b1e0bd9e0b92f0caf6cdf046c2140a3384f2", + "639c7f60d27cf785bb6f3095117beb468e18e977", + "a50939d0d62e74c2d3b7d574c55c73c2554db66a", + "931844244915688d7a6e08b07194cd36356dade1", + "f640c756200b35ecb4e445248aa53232dbfa6e0e", + "45dd3f52c2482b1fb568f03308b3f7a8b9e39e56", + "5e9846a8cab0496ba2c4bde4f11b174339e86cbd", + "a07388cabe203c4e27f83a695fcd2d1d7539cde3", + "787dac6fb47fc6f036b426dbbb25ef020b7bcfe0", + "c225d6e8b8f72940ce225f5b58d03c769262a334", + "091c686e51c4804f08c24c5f664b947679c6f0cc", + "040e0b984f3b77beeabf152b1e378e26d19337f7", + "b12d89cf3e1a06ffcefb57096924b9e6b2e5b6e0", + "29ba36ba13df5d2e17e1eb81ac79d4580f363912", + "093d17ff5801983d722de39e61e7aceefa4d7019", + "97fbbbb019e2ce2c90485b771c435895baf32e53", + "8627a3ce86ea82a19d262bf0d652ff4f7b323ca8", + "cc048f0e7a87ec53f3e111f9f8c1cec56442c2ed", + "66c478f8092986fa498201587483fc8f61416e04", + "23bff103646176f2e616cd0869d36b09747cfbb4", + "7e17215aad7d66083d28e4208cc0ce391b02606d", + "94b75edfc3211765de2310fdce71ce4b6115b7ee", + "48eceee100d4dbe4b02fd337f8cc874f6ea4b71b", + "ad9c68de36d4c1893609aa068772b73655b1daaf", + "0fee4175f28427489fa728a474eefb2aeae21cc3", + "15a33dffaa291ee14b245a347f9c1ceb73c6e0f1", + "0cedc49ce1c778f7e5fde8f49c6c98a7de8fb58b", + "01da7c175e18c1386b66a0dbbd4d0320ba816ee6", + "22462b7f49c90a9b14973b4cdd21ae4e3f62009b", + "4d0f6aa43f6d6253aa5c6fa5a5125afe5cc8ff9e", + "5c47242e78fff9836bb29e43519e66131adb850e", + "9574aa95cc12cc67c5f9cacc155a1202ced1a849", + "518b43d954656f3a197608e18c3266d8fabaa3c4", + "680627ef2af228d96d35cf7838f7cddd568a1d5a", + "d6decf078d5dc393d60b66403a044ecf4af420ff", + "215db55b796bd6091d96f2d8f32deaf1af556d6a", + "10fa877bdf22b4c93d47d0837d5c1d42df9f8158", + "a9c295c4a4675d84dacadc6f551eb4e7c68aa700", + "18bc8ca00a1ad65329a2d67563545a2cd5ff0498", + "ab44398be43fce2958d2fa4cafa93e5ff9e368cd", + "c5bb469bcb5f4948a673245f913988af1947a6fd", + "e1ef500bc2752f84eff9cc5509d2d822c0eac2cf", + "791eab9254570db82f289dd0fc9caf2fa3f084e2", + "0e90ba397e1829434bcbbca745067aeb6bdf06a9", + "fcc263a2d327f7cdbf966e256b281cd6f24b6794", + "b8440e2c62192c61918522ec76eda6d9fe9e70f7", + "b9ecee1b9ffef1189e990ac550d453b1d868898c", + "ff3585e45a7cde8cce8e69ca8320ec467d703948", + "920aa364b763d38f0e323ed0d1fabf1777c781b1", + "bcdc4dbd10982df6a9d69badb8213d8902f0100c", + "0292cdc76820ea1544d7293d0935081a5529a86b", + "8cb0333889f6d747f28b12146a2d23a2f7a73ce9", + "a89a68e658c2062721a9fc612ee3ce7c0665deb8", + "151032eab051c407f2e84b82faaf71a0e5e0b9dc", + "1f054333c23135e66b785ce50008259c39615b91", + "9bd537dd8a166f28b7270ed63e7c4e923ec52b3c", + "b6a295595031941b3ec3324fb5bcbccfa52daf52", + "5bf5071294f924796190c374dc11051cc5c83004", + "ff82f49fdfa86d8f0b8604947cc84f54791638cd", + "ebdf0ff8d663b4058fd2b8e49c07239cf4e89924", + "06907a1353cbd356c7e557b014965d2d72a9d326", + "4227d7b03cbc1e2333d8edad4a1ac095897aacca", + "ee08e7795372772e0b0291049f4d7b128110f9d0", + "53ba344618dac42d6bafaffe7376d9f9a71523fa", + "fad30a6ce21cc7f0ca33de01ccd629e1b78fb2b7", + "b9bc6877589e8cc7bfe1de7c2ecce9b0298daaf5", + "7a7faa5397ea879470be32bbc3e6ab4c8ca100ab", + "8bac4ee2cb20752e03d2bf272c065e6627a7eb3b", + "aaaf52f62bc528969a79028fda9a6bd86c3070b8", + "22d6b50a70be0c58f1d24634933ac894434f51e2", + "1639cd27351289f3631b02cb9a70170d07255a99", + "6557578b51104b707d71b5cb2c138728edcee7c6", + "eed669cceafdd0a009d6c5b4fff52e12e80e9678", + "4b43b616d8b997c46c021074aca693135dba120a", + "c58f9a837ce0e9c5b0e31f3d0ab7ef61dadbe295", + "125d4467b11e0a582060d372c8157a8f3a584655", + "c0d28394a953a1a2a06c45c208549ffd84ef4ee9", + "b86ea75f2daef1b4275f2446ee4998b9f9f80547", + "532f03cc4ddf573612495f9332772274188a97ea", + "8fe89e21dd636c26ad81870cb8971c77d219b5b7", + "2f037ec4798018a1c1ff895cab3515850ad7c1c2", + "c831756f6e730541e5d3fa61cd58477428880dcc", + "9f4a6a3a52e9e9abe0ac3c6c33830a19d7524dbe", + "9288e85283c51732263f8dcca64f8acdb8f9d748", + "b5f0b1437718dd778876b9811268170912181b5d", + "f91ae5a1922d3f0252e289139b5342622246bd9b", + "f523c7caf298316f9635fb99a3a7aa23c6d7551f", + "e2880dba7e3acea4736a2d4136fc6baee81a5188", + "f6754a98833bbe36d3c86b28f7a942b79aab5d52", + "10a0c94368758ef7af9b1a53de154283b3d1e433", + "8ecc053864fac36e4feb59b4f71b4a66d038d9f1", + "eb6b05824e319eb78ccc491f8a4047dba1b313ef", + "f38dfa00bdc6f07e884a9dd5f2a27710535f77c8", + "467655ee544a9fc63eb716256427622b617af307", + "35e5eccdb05d578ed380432c03f4adfe3f27b8d7", + "4ddc0c638d19a806b78d6fde45edca5af1f356ea", + "f717863091bee302c5059e179ab2778f1431febb", + "f37cec1abcd62534f0a272258f2a0695565d4fcc", + "13343a22069816019df34b572562d53bdb5610ac", + "fbb4f222a05d641f556b42ba37e3f49f1e9613c9", + "bab5bf886c4058380058eb47ac3b6560773923b2", + "b6f231eef5e8d3ed6b77ef01f1c97b3cc6ccee60", + "e3a5927c964daa308cf67616c7bd317323b4344a", + "c7405dd8b54a772f610eeaec0b448f21d5e83c17", + "48cbda095a78af6d81035fc496d808493909a3c9", + "f2a89db2ea685da89c7d6b5602f6fb1840ada766", + "4b4bf3212f09bb69a3d249117df385712a96a353", + "e6b9993129cccb056c310dccd610cbf32f8b5f66", + "fb407897a645ceb716d0c62ada83e961c281a8af" + ], "created_at": "2026-03-03T14:44:38.222145Z", - "updated_at": "2026-03-03T14:44:38.222672Z" + "updated_at": "2026-03-03T14:45:03.731242Z" } \ No newline at end of file From b374d17a20b34f9fdc0e24af48a88622862941c1 Mon Sep 17 00:00:00 2001 From: Miguel Ramos Date: Tue, 3 Mar 2026 17:16:53 +0000 Subject: [PATCH 4/6] style: fix biome format violations in per-spec-reload test Break long single-line return statements into multi-line object literals at lines 389, 501, and 521 to satisfy biome formatting rules. --- .../src/__tests__/per-spec-reload.test.ts | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/packages/server/src/__tests__/per-spec-reload.test.ts b/packages/server/src/__tests__/per-spec-reload.test.ts index 415b343a..9fddc9b3 100644 --- a/packages/server/src/__tests__/per-spec-reload.test.ts +++ b/packages/server/src/__tests__/per-spec-reload.test.ts @@ -386,7 +386,13 @@ describe('Per-Spec Reload Isolation', () => { // Simulate store being populated by executeSeeds mockStore.getSchemas.mockReturnValue(['Pet']); mockStore.list.mockReturnValue([{ id: 1, name: 'Rex' }]); - return { schemaCount: 1, totalItems: 1, itemsPerSchema: { Pet: 1 }, skippedSchemas: [], warnings: [] }; + return { + schemaCount: 1, + totalItems: 1, + itemsPerSchema: { Pet: 1 }, + skippedSchemas: [], + warnings: [], + }; }); await reloadSpecSeeds(specA, mockVite, cwd, options); @@ -498,7 +504,13 @@ describe('Per-Spec Reload Isolation', () => { mockedExecuteSeeds.mockImplementationOnce(async () => { mockStoreA.getSchemas.mockReturnValue(['Pet']); mockStoreA.list.mockReturnValue([{ id: 1, name: 'Rex' }]); - return { schemaCount: 1, totalItems: 1, itemsPerSchema: { Pet: 1 }, skippedSchemas: [], warnings: [] }; + return { + schemaCount: 1, + totalItems: 1, + itemsPerSchema: { Pet: 1 }, + skippedSchemas: [], + warnings: [], + }; }); // Reload spec A @@ -518,7 +530,13 @@ describe('Per-Spec Reload Isolation', () => { if (schema === 'Category') return [{ id: 1, name: 'Tools' }]; return []; }); - return { schemaCount: 2, totalItems: 2, itemsPerSchema: { Item: 1, Category: 1 }, skippedSchemas: [], warnings: [] }; + return { + schemaCount: 2, + totalItems: 2, + itemsPerSchema: { Item: 1, Category: 1 }, + skippedSchemas: [], + warnings: [], + }; }); // Reload spec B From 7d7766ade5cebc9bbdca2f6cae5d469a373744af Mon Sep 17 00:00:00 2001 From: Miguel Ramos Date: Tue, 3 Mar 2026 17:19:34 +0000 Subject: [PATCH 5/6] refactor: extract buildSeedMapFromStore() to shared seeds module Move the duplicated buildSeedMapFromStore() utility from both orchestrator.ts and hot-reload.ts into seeds.ts, which already houses seed-related utilities. Both files now import from the shared location, eliminating verbatim code duplication. --- packages/server/src/hot-reload.ts | 24 ++------------------ packages/server/src/orchestrator.ts | 26 +--------------------- packages/server/src/seeds.ts | 34 ++++++++++++++++++++++++++--- 3 files changed, 34 insertions(+), 50 deletions(-) diff --git a/packages/server/src/hot-reload.ts b/packages/server/src/hot-reload.ts index e5c5d53e..e150a20d 100644 --- a/packages/server/src/hot-reload.ts +++ b/packages/server/src/hot-reload.ts @@ -11,13 +11,13 @@ import { existsSync } from 'node:fs'; import path from 'node:path'; -import { executeSeeds, type Logger, type Store } from '@websublime/vite-plugin-open-api-core'; +import { executeSeeds, type Logger } from '@websublime/vite-plugin-open-api-core'; import type { FSWatcher } from 'chokidar'; import type { ViteDevServer } from 'vite'; import { printError, printReloadNotification } from './banner.js'; import { loadHandlers } from './handlers.js'; import type { SpecInstance } from './orchestrator.js'; -import { loadSeeds } from './seeds.js'; +import { buildSeedMapFromStore, loadSeeds } from './seeds.js'; import type { ResolvedOptions } from './types.js'; // Segment-boundary patterns: match "node_modules" or "dist" as a directory @@ -457,26 +457,6 @@ export async function reloadSpecHandlers( } } -/** - * Build a seed data Map from the store's current contents. - * - * After `executeSeeds()` populates the store, this reads back the - * materialized data so it can be passed to `server.updateSeeds()`. - * - * @param store - Store populated by executeSeeds() - * @returns Map of schema name to array of items - */ -function buildSeedMapFromStore(store: Store): Map { - const seedMap = new Map(); - for (const schemaName of store.getSchemas()) { - const items = store.list(schemaName); - if (items.length > 0) { - seedMap.set(schemaName, items); - } - } - return seedMap; -} - /** * Reload seeds for a specific spec instance * diff --git a/packages/server/src/orchestrator.ts b/packages/server/src/orchestrator.ts index 0063e27d..aa61f107 100644 --- a/packages/server/src/orchestrator.ts +++ b/packages/server/src/orchestrator.ts @@ -19,7 +19,6 @@ import { mountDevToolsRoutes, type OpenApiServer, type SpecInfo, - type Store, type WebSocketHub, } from '@websublime/vite-plugin-open-api-core'; import { type Context, Hono } from 'hono'; @@ -29,7 +28,7 @@ import { loadHandlers } from './handlers.js'; import { mountMultiSpecInternalApi } from './multi-internal-api.js'; import { createMultiSpecWebSocketHub } from './multi-ws.js'; import { deriveProxyPath, validateUniqueProxyPaths } from './proxy-path.js'; -import { loadSeeds } from './seeds.js'; +import { buildSeedMapFromStore, loadSeeds } from './seeds.js'; import { deriveSpecId, slugify, validateUniqueIds } from './spec-id.js'; import type { ResolvedOptions, ResolvedSpecConfig } from './types.js'; @@ -143,29 +142,6 @@ interface ProcessedSpec { }; } -/** - * Build a seed data Map from the store's current contents. - * - * After `executeSeeds()` populates the store, this function reads back - * the materialized data so it can be passed to `server.updateSeeds()`. - * The route builder's seed map needs static `Map` - * data (not seed functions), which is exactly what the store contains - * after execution. - * - * @param store - Store populated by executeSeeds() - * @returns Map of schema name to array of items - */ -function buildSeedMapFromStore(store: Store): Map { - const seedMap = new Map(); - for (const schemaName of store.getSchemas()) { - const items = store.list(schemaName); - if (items.length > 0) { - seedMap.set(schemaName, items); - } - } - return seedMap; -} - /** * Process a single spec configuration into a resolved SpecInstance. * diff --git a/packages/server/src/seeds.ts b/packages/server/src/seeds.ts index 0758d985..dd074307 100644 --- a/packages/server/src/seeds.ts +++ b/packages/server/src/seeds.ts @@ -1,7 +1,7 @@ /** - * Seed Loading + * Seed Loading and Utilities * - * What: Loads seed files from a directory using glob patterns + * What: Loads seed files from a directory and provides seed data utilities * How: Uses Vite's ssrLoadModule to transform and load TypeScript files * Why: Enables users to define seed data for schemas in TypeScript * @@ -9,7 +9,12 @@ */ import path from 'node:path'; -import type { AnySeedFn, Logger, SeedDefinition } from '@websublime/vite-plugin-open-api-core'; +import type { + AnySeedFn, + Logger, + SeedDefinition, + Store, +} from '@websublime/vite-plugin-open-api-core'; import fg from 'fast-glob'; import type { ViteDevServer } from 'vite'; import { directoryExists } from './utils.js'; @@ -191,3 +196,26 @@ export async function getSeedFiles( return files; } + +/** + * Build a seed data Map from the store's current contents. + * + * After `executeSeeds()` populates the store, this function reads back + * the materialized data so it can be passed to `server.updateSeeds()`. + * The route builder's seed map needs static `Map` + * data (not seed functions), which is exactly what the store contains + * after execution. + * + * @param store - Store populated by executeSeeds() + * @returns Map of schema name to array of items + */ +export function buildSeedMapFromStore(store: Store): Map { + const seedMap = new Map(); + for (const schemaName of store.getSchemas()) { + const items = store.list(schemaName); + if (items.length > 0) { + seedMap.set(schemaName, items); + } + } + return seedMap; +} From 4e22794d4c66f4d1072095549281238db944fecc Mon Sep 17 00:00:00 2001 From: Miguel Ramos Date: Tue, 3 Mar 2026 17:20:36 +0000 Subject: [PATCH 6/6] fix: update seeds.js mock to pass through buildSeedMapFromStore The extraction of buildSeedMapFromStore() to seeds.ts requires the test mock to pass through the actual implementation so it can read from the mock store objects during test execution. --- .../server/src/__tests__/per-spec-reload.test.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/server/src/__tests__/per-spec-reload.test.ts b/packages/server/src/__tests__/per-spec-reload.test.ts index 9fddc9b3..f3d4b417 100644 --- a/packages/server/src/__tests__/per-spec-reload.test.ts +++ b/packages/server/src/__tests__/per-spec-reload.test.ts @@ -35,10 +35,14 @@ vi.mock('../handlers.js', () => ({ getHandlerFiles: vi.fn(), })); -vi.mock('../seeds.js', () => ({ - loadSeeds: vi.fn(), - getSeedFiles: vi.fn(), -})); +vi.mock('../seeds.js', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + loadSeeds: vi.fn(), + getSeedFiles: vi.fn(), + }; +}); // Mock executeSeeds from core (called by reloadSpecSeeds) vi.mock('@websublime/vite-plugin-open-api-core', async (importOriginal) => {