@foldkit/vite-plugin 0.11.1 → 0.11.3

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;AAglBF;;;;;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';
@@ -18,6 +18,7 @@ export { foldkitViewIdentity, transformViewIdentity, } from './viewIdentity.js';
18
18
  // source by `scripts/check-effect-prebundle.ts` (runs in `pnpm check`).
19
19
  const FORCE_INCLUDED_EFFECT_NAMESPACES = [
20
20
  'effect/Array',
21
+ 'effect/Boolean',
21
22
  'effect/Cause',
22
23
  'effect/Clock',
23
24
  'effect/Context',
@@ -81,7 +82,7 @@ const resolveInstalledFoldkitPackages = (root) => {
81
82
  }
82
83
  catch (error) {
83
84
  return !(error instanceof Error &&
84
- 'code' in error &&
85
+ Predicate.hasProperty(error, 'code') &&
85
86
  error.code === 'MODULE_NOT_FOUND');
86
87
  }
87
88
  });
@@ -239,20 +240,18 @@ const registerViteWsHandlers = (server, state, enqueue) => Effect.sync(() => {
239
240
  server.ws.on('foldkit:devTools:response', (data) => enqueue(Event.BrowserResponseFrameReceived({ data })));
240
241
  });
241
242
  // MCP RELAY
242
- const startMcpRelay = (port, enqueue) => Effect.acquireRelease(Effect.sync(() => {
243
+ // NOTE: Restarting a dev server briefly leaves two of them alive. Vite builds
244
+ // the replacement, which binds its relay, before closing the server it
245
+ // replaces, which still owns the port. The bind loses that race and has to
246
+ // wait for the outgoing server to release the port, so it retries for four
247
+ // seconds before reporting the port as taken.
248
+ const RELAY_BIND_RETRY_DELAY = Duration.millis(100);
249
+ const RELAY_BIND_RETRY_COUNT = 40;
250
+ class RelayBindFailed extends Data.TaggedError('RelayBindFailed') {
251
+ }
252
+ const isPortInUse = (error) => Predicate.hasProperty(error, 'code') && error.code === 'EADDRINUSE';
253
+ const bindMcpRelay = (port, enqueue) => Effect.callback(resume => {
243
254
  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
255
  wss.on('connection', client => {
257
256
  enqueue(Event.McpClientConnected({ client }));
258
257
  client.on('message', raw => enqueue(Event.McpRequestReceived({ client, raw: raw.toString() })));
@@ -261,14 +260,45 @@ const startMcpRelay = (port, enqueue) => Effect.acquireRelease(Effect.sync(() =>
261
260
  console.error('[foldkit:devTools] MCP client error', error);
262
261
  });
263
262
  });
264
- wss.on('listening', () => {
263
+ const onListening = () => {
264
+ wss.off('error', onBindFailed);
265
+ wss.on('error', error => {
266
+ console.error('[foldkit:devTools] MCP relay error', error);
267
+ });
265
268
  console.log(`[foldkit:devTools] MCP relay listening on ws://localhost:${port}`);
266
- });
267
- return wss;
268
- }), wss => Effect.sync(() => {
269
+ resume(Effect.succeed(wss));
270
+ };
271
+ const onBindFailed = (cause) => {
272
+ wss.off('listening', onListening);
273
+ wss.close();
274
+ resume(Effect.fail(new RelayBindFailed({ cause })));
275
+ };
276
+ wss.once('listening', onListening);
277
+ wss.once('error', onBindFailed);
278
+ });
279
+ const reportRelayBindFailed = (port, cause) => {
280
+ if (isPortInUse(cause)) {
281
+ return Console.error(`\n[foldkit:devTools] Port ${port} is already in use, so the DevTools MCP relay could not start.\n` +
282
+ `[foldkit:devTools] This usually means another Foldkit project is already running and bound to this port.\n` +
283
+ `[foldkit:devTools] Until the port is freed, agents will not be able to connect to this app via the Foldkit DevTools MCP server.\n` +
284
+ `[foldkit:devTools] Stop the other project, or set a different \`devToolsMcpPort\` in this project's vite config.\n` +
285
+ `[foldkit:devTools] If you change \`devToolsMcpPort\`, also set \`FOLDKIT_DEVTOOLS_MCP_PORT\` to the same value for your MCP server.\n`);
286
+ }
287
+ else {
288
+ return Console.error(`[foldkit:devTools] MCP relay failed to start on port ${port}; continuing without the relay`, cause);
289
+ }
290
+ };
291
+ const startMcpRelay = (port, enqueue) => Effect.acquireRelease(bindMcpRelay(port, enqueue), wss => Effect.gen(function* () {
292
+ for (const client of wss.clients) {
293
+ client.terminate();
294
+ }
269
295
  wss.close();
270
- console.log('[foldkit:devTools] MCP relay stopped');
271
- }));
296
+ yield* Console.log('[foldkit:devTools] MCP relay stopped');
297
+ })).pipe(Effect.retry({
298
+ while: ({ cause }) => isPortInUse(cause),
299
+ times: RELAY_BIND_RETRY_COUNT,
300
+ schedule: Schedule.spaced(RELAY_BIND_RETRY_DELAY),
301
+ }), Effect.catchTag('RelayBindFailed', ({ cause }) => reportRelayBindFailed(port, cause)));
272
302
  // PROGRAM
273
303
  const main = (server, events, options) => Effect.gen(function* () {
274
304
  const state = yield* makeState;
@@ -276,8 +306,13 @@ const main = (server, events, options) => Effect.gen(function* () {
276
306
  Queue.offerUnsafe(events, event);
277
307
  };
278
308
  yield* registerViteWsHandlers(server, state, enqueue);
309
+ // NOTE: Forked rather than awaited because binding the relay can retry for
310
+ // seconds. The HMR bridge is independent of the relay, and the runtime
311
+ // gives up on its boot-time model request in well under a second, so
312
+ // sequencing the dispatch loop behind the bind would cost model
313
+ // preservation whenever the port is contended.
279
314
  if (options.devToolsMcpPort !== undefined) {
280
- yield* startMcpRelay(options.devToolsMcpPort, enqueue);
315
+ yield* Effect.forkScoped(startMcpRelay(options.devToolsMcpPort, enqueue));
281
316
  }
282
317
  yield* Stream.fromQueue(events).pipe(Stream.runForEach(event => dispatchEvent(server, state, event)));
283
318
  });
@@ -290,6 +325,21 @@ const main = (server, events, options) => Effect.gen(function* () {
290
325
  */
291
326
  export const foldkit = (options = {}) => {
292
327
  const events = Effect.runSync(Queue.unbounded());
328
+ // NOTE: One plugin instance can serve more than one dev server, and on
329
+ // restart Vite builds the replacement, running `configureServer` again,
330
+ // before closing the server being replaced. Keying by resolved config keeps
331
+ // each server's shutdown pointed at its own fiber.
332
+ const mainFibers = new WeakMap();
333
+ const stopMain = (config) => Effect.suspend(() => {
334
+ const fiber = mainFibers.get(config);
335
+ mainFibers.delete(config);
336
+ if (fiber === undefined) {
337
+ return Effect.void;
338
+ }
339
+ else {
340
+ return Fiber.interrupt(fiber);
341
+ }
342
+ });
293
343
  const hmrPlugin = {
294
344
  name: 'foldkit-hmr',
295
345
  apply: 'serve',
@@ -303,9 +353,14 @@ export const foldkit = (options = {}) => {
303
353
  }),
304
354
  configureServer: server => {
305
355
  const fiber = Effect.runFork(Effect.scoped(main(server, events, options)));
306
- server.httpServer?.on('close', () => {
307
- Effect.runFork(Fiber.interrupt(fiber));
308
- });
356
+ mainFibers.set(server.config, fiber);
357
+ },
358
+ // NOTE: Vite awaits `closeBundle` when the dev server closes, once per
359
+ // environment plugin container. Hanging shutdown off `server.httpServer`
360
+ // instead would never run in middleware mode, which is how Vitest and
361
+ // other embedders run Vite, and the relay would outlive the server.
362
+ closeBundle() {
363
+ return Effect.runPromise(stopMain(this.environment.getTopLevelConfig()));
309
364
  },
310
365
  handleHotUpdate: ({ server, modules, }) => {
311
366
  if (modules.length === 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foldkit/vite-plugin",
3
- "version": "0.11.1",
3
+ "version": "0.11.3",
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.101",
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.101",
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.132.0"
36
+ "foldkit": "0.135.0"
37
37
  },
38
38
  "keywords": [
39
39
  "vite",