@foldkit/vite-plugin 0.11.0 → 0.11.2

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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAiCA,OAAO,KAAK,EAAE,MAAM,EAAkC,MAAM,MAAM,CAAA;AAKlE,OAAO,EAAE,KAAK,eAAe,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AACzE,OAAO,EACL,KAAK,2BAA2B,EAChC,mBAAmB,EACnB,qBAAqB,GACtB,MAAM,mBAAmB,CAAA;AAE1B,6CAA6C;AAC7C,MAAM,MAAM,oBAAoB,GAAG,QAAQ,CAAC;IAC1C;;;;;OAKG;IACH,eAAe,CAAC,EAAE,MAAM,CAAA;CACzB,CAAC,CAAA;AAgiBF;;;;;GAKG;AACH,eAAO,MAAM,OAAO,GAAI,UAAS,oBAAyB,KAAG,KAAK,CAAC,MAAM,CAuCxE,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAoCA,OAAO,KAAK,EACV,MAAM,EAIP,MAAM,MAAM,CAAA;AAKb,OAAO,EAAE,KAAK,eAAe,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AACzE,OAAO,EACL,KAAK,2BAA2B,EAChC,mBAAmB,EACnB,qBAAqB,GACtB,MAAM,mBAAmB,CAAA;AAE1B,6CAA6C;AAC7C,MAAM,MAAM,oBAAoB,GAAG,QAAQ,CAAC;IAC1C;;;;;OAKG;IACH,eAAe,CAAC,EAAE,MAAM,CAAA;CACzB,CAAC,CAAA;AA+kBF;;;;;GAKG;AACH,eAAO,MAAM,OAAO,GAAI,UAAS,oBAAyB,KAAG,KAAK,CAAC,MAAM,CA6DxE,CAAA"}
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { Array, Console, Data, Effect, Exit, Fiber, HashMap, HashSet, Match as M, Option, Queue, Ref, Schema as S, Stream, pipe, } from 'effect';
1
+ import { Array, Console, Data, Duration, Effect, Exit, Fiber, HashMap, HashSet, Match as M, Option, Predicate, Queue, Ref, Schema as S, Schedule, Stream, pipe, } from 'effect';
2
2
  import { EventFrame, RequestFrame, ResponseFrame, ResponseRuntimes, } from 'foldkit/devtools-protocol';
3
3
  import { PreserveModelMessage, RequestModelMessage, RestoreModelMessage, } from 'foldkit/hmr-protocol';
4
4
  import { createRequire } from 'node:module';
@@ -81,7 +81,7 @@ const resolveInstalledFoldkitPackages = (root) => {
81
81
  }
82
82
  catch (error) {
83
83
  return !(error instanceof Error &&
84
- 'code' in error &&
84
+ Predicate.hasProperty(error, 'code') &&
85
85
  error.code === 'MODULE_NOT_FOUND');
86
86
  }
87
87
  });
@@ -239,20 +239,18 @@ const registerViteWsHandlers = (server, state, enqueue) => Effect.sync(() => {
239
239
  server.ws.on('foldkit:devTools:response', (data) => enqueue(Event.BrowserResponseFrameReceived({ data })));
240
240
  });
241
241
  // MCP RELAY
242
- const startMcpRelay = (port, enqueue) => Effect.acquireRelease(Effect.sync(() => {
242
+ // NOTE: Restarting a dev server briefly leaves two of them alive. Vite builds
243
+ // the replacement, which binds its relay, before closing the server it
244
+ // replaces, which still owns the port. The bind loses that race and has to
245
+ // wait for the outgoing server to release the port, so it retries for four
246
+ // seconds before reporting the port as taken.
247
+ const RELAY_BIND_RETRY_DELAY = Duration.millis(100);
248
+ const RELAY_BIND_RETRY_COUNT = 40;
249
+ class RelayBindFailed extends Data.TaggedError('RelayBindFailed') {
250
+ }
251
+ const isPortInUse = (error) => Predicate.hasProperty(error, 'code') && error.code === 'EADDRINUSE';
252
+ const bindMcpRelay = (port, enqueue) => Effect.callback(resume => {
243
253
  const wss = new WebSocketServer({ port });
244
- wss.on('error', error => {
245
- if ('code' in error && error.code === 'EADDRINUSE') {
246
- console.error(`\n[foldkit:devTools] Port ${port} is already in use, so the DevTools MCP relay could not start.\n` +
247
- `[foldkit:devTools] This usually means another Foldkit project is already running and bound to this port.\n` +
248
- `[foldkit:devTools] Until the port is freed, agents will not be able to connect to this app via the Foldkit DevTools MCP server.\n` +
249
- `[foldkit:devTools] Stop the other project, or set a different \`devToolsMcpPort\` in this project's vite config.\n` +
250
- `[foldkit:devTools] If you change \`devToolsMcpPort\`, also set \`FOLDKIT_DEVTOOLS_MCP_PORT\` to the same value for your MCP server.\n`);
251
- }
252
- else {
253
- console.error(`[foldkit:devTools] MCP relay failed to start on port ${port}; continuing without the relay`, error);
254
- }
255
- });
256
254
  wss.on('connection', client => {
257
255
  enqueue(Event.McpClientConnected({ client }));
258
256
  client.on('message', raw => enqueue(Event.McpRequestReceived({ client, raw: raw.toString() })));
@@ -261,14 +259,45 @@ const startMcpRelay = (port, enqueue) => Effect.acquireRelease(Effect.sync(() =>
261
259
  console.error('[foldkit:devTools] MCP client error', error);
262
260
  });
263
261
  });
264
- wss.on('listening', () => {
262
+ const onListening = () => {
263
+ wss.off('error', onBindFailed);
264
+ wss.on('error', error => {
265
+ console.error('[foldkit:devTools] MCP relay error', error);
266
+ });
265
267
  console.log(`[foldkit:devTools] MCP relay listening on ws://localhost:${port}`);
266
- });
267
- return wss;
268
- }), wss => Effect.sync(() => {
268
+ resume(Effect.succeed(wss));
269
+ };
270
+ const onBindFailed = (cause) => {
271
+ wss.off('listening', onListening);
272
+ wss.close();
273
+ resume(Effect.fail(new RelayBindFailed({ cause })));
274
+ };
275
+ wss.once('listening', onListening);
276
+ wss.once('error', onBindFailed);
277
+ });
278
+ const reportRelayBindFailed = (port, cause) => {
279
+ if (isPortInUse(cause)) {
280
+ return Console.error(`\n[foldkit:devTools] Port ${port} is already in use, so the DevTools MCP relay could not start.\n` +
281
+ `[foldkit:devTools] This usually means another Foldkit project is already running and bound to this port.\n` +
282
+ `[foldkit:devTools] Until the port is freed, agents will not be able to connect to this app via the Foldkit DevTools MCP server.\n` +
283
+ `[foldkit:devTools] Stop the other project, or set a different \`devToolsMcpPort\` in this project's vite config.\n` +
284
+ `[foldkit:devTools] If you change \`devToolsMcpPort\`, also set \`FOLDKIT_DEVTOOLS_MCP_PORT\` to the same value for your MCP server.\n`);
285
+ }
286
+ else {
287
+ return Console.error(`[foldkit:devTools] MCP relay failed to start on port ${port}; continuing without the relay`, cause);
288
+ }
289
+ };
290
+ const startMcpRelay = (port, enqueue) => Effect.acquireRelease(bindMcpRelay(port, enqueue), wss => Effect.gen(function* () {
291
+ for (const client of wss.clients) {
292
+ client.terminate();
293
+ }
269
294
  wss.close();
270
- console.log('[foldkit:devTools] MCP relay stopped');
271
- }));
295
+ yield* Console.log('[foldkit:devTools] MCP relay stopped');
296
+ })).pipe(Effect.retry({
297
+ while: ({ cause }) => isPortInUse(cause),
298
+ times: RELAY_BIND_RETRY_COUNT,
299
+ schedule: Schedule.spaced(RELAY_BIND_RETRY_DELAY),
300
+ }), Effect.catchTag('RelayBindFailed', ({ cause }) => reportRelayBindFailed(port, cause)));
272
301
  // PROGRAM
273
302
  const main = (server, events, options) => Effect.gen(function* () {
274
303
  const state = yield* makeState;
@@ -276,8 +305,13 @@ const main = (server, events, options) => Effect.gen(function* () {
276
305
  Queue.offerUnsafe(events, event);
277
306
  };
278
307
  yield* registerViteWsHandlers(server, state, enqueue);
308
+ // NOTE: Forked rather than awaited because binding the relay can retry for
309
+ // seconds. The HMR bridge is independent of the relay, and the runtime
310
+ // gives up on its boot-time model request in well under a second, so
311
+ // sequencing the dispatch loop behind the bind would cost model
312
+ // preservation whenever the port is contended.
279
313
  if (options.devToolsMcpPort !== undefined) {
280
- yield* startMcpRelay(options.devToolsMcpPort, enqueue);
314
+ yield* Effect.forkScoped(startMcpRelay(options.devToolsMcpPort, enqueue));
281
315
  }
282
316
  yield* Stream.fromQueue(events).pipe(Stream.runForEach(event => dispatchEvent(server, state, event)));
283
317
  });
@@ -290,6 +324,21 @@ const main = (server, events, options) => Effect.gen(function* () {
290
324
  */
291
325
  export const foldkit = (options = {}) => {
292
326
  const events = Effect.runSync(Queue.unbounded());
327
+ // NOTE: One plugin instance can serve more than one dev server, and on
328
+ // restart Vite builds the replacement, running `configureServer` again,
329
+ // before closing the server being replaced. Keying by resolved config keeps
330
+ // each server's shutdown pointed at its own fiber.
331
+ const mainFibers = new WeakMap();
332
+ const stopMain = (config) => Effect.suspend(() => {
333
+ const fiber = mainFibers.get(config);
334
+ mainFibers.delete(config);
335
+ if (fiber === undefined) {
336
+ return Effect.void;
337
+ }
338
+ else {
339
+ return Fiber.interrupt(fiber);
340
+ }
341
+ });
293
342
  const hmrPlugin = {
294
343
  name: 'foldkit-hmr',
295
344
  apply: 'serve',
@@ -303,9 +352,14 @@ export const foldkit = (options = {}) => {
303
352
  }),
304
353
  configureServer: server => {
305
354
  const fiber = Effect.runFork(Effect.scoped(main(server, events, options)));
306
- server.httpServer?.on('close', () => {
307
- Effect.runFork(Fiber.interrupt(fiber));
308
- });
355
+ mainFibers.set(server.config, fiber);
356
+ },
357
+ // NOTE: Vite awaits `closeBundle` when the dev server closes, once per
358
+ // environment plugin container. Hanging shutdown off `server.httpServer`
359
+ // instead would never run in middleware mode, which is how Vitest and
360
+ // other embedders run Vite, and the relay would outlive the server.
361
+ closeBundle() {
362
+ return Effect.runPromise(stopMain(this.environment.getTopLevelConfig()));
309
363
  },
310
364
  handleHotUpdate: ({ server, modules, }) => {
311
365
  if (modules.length === 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foldkit/vite-plugin",
3
- "version": "0.11.0",
3
+ "version": "0.11.2",
4
4
  "description": "Vite plugin for Foldkit hot module reloading with state preservation",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -16,7 +16,7 @@
16
16
  "dist"
17
17
  ],
18
18
  "peerDependencies": {
19
- "effect": "4.0.0-beta.97",
19
+ "effect": "4.0.0-beta.102",
20
20
  "foldkit": "^0",
21
21
  "vite": "^7.0.0 || ^8.0.0"
22
22
  },
@@ -27,13 +27,13 @@
27
27
  "devDependencies": {
28
28
  "@types/node": "^25.9.3",
29
29
  "@types/ws": "^8.18.1",
30
- "effect": "4.0.0-beta.97",
30
+ "effect": "4.0.0-beta.102",
31
31
  "happy-dom": "^20.10.4",
32
32
  "rimraf": "^6.1.3",
33
33
  "typescript": "^6.0.3",
34
34
  "vite": "^8.0.16",
35
35
  "vitest": "^4.1.9",
36
- "foldkit": "0.130.0"
36
+ "foldkit": "0.133.0"
37
37
  },
38
38
  "keywords": [
39
39
  "vite",