@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.
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +80 -25
- package/package.json +4 -4
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
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'
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
268
|
-
|
|
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
|
-
|
|
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.
|
|
307
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
36
|
+
"foldkit": "0.135.0"
|
|
37
37
|
},
|
|
38
38
|
"keywords": [
|
|
39
39
|
"vite",
|