@myrialabs/clopen 0.0.6 → 0.0.7

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/.env.example CHANGED
@@ -3,4 +3,10 @@ NODE_ENV=production
3
3
 
4
4
  # Server configuration
5
5
  HOST=localhost
6
- PORT=9141
6
+
7
+ # Production port (single port — Elysia serves everything)
8
+ PORT=9141
9
+
10
+ # Development ports (two ports — Vite proxies to Elysia)
11
+ PORT_BACKEND=9151
12
+ PORT_FRONTEND=9141
package/backend/index.ts CHANGED
@@ -11,10 +11,9 @@ import { loggerMiddleware } from './middleware/logger';
11
11
  import { initializeDatabase, closeDatabase } from './lib/database';
12
12
  import { disposeAllEngines } from './lib/engine';
13
13
  import { debug } from '$shared/utils/logger';
14
- import { isPortInUse } from './lib/shared/port-utils';
14
+ import { findAvailablePort } from './lib/shared/port-utils';
15
15
  import { networkInterfaces } from 'os';
16
16
  import { resolve } from 'node:path';
17
- import { statSync } from 'node:fs';
18
17
 
19
18
  // Import WebSocket router
20
19
  import { wsRouter } from './ws';
@@ -22,9 +21,8 @@ import { wsRouter } from './ws';
22
21
  /**
23
22
  * Clopen - Elysia Backend Server
24
23
  *
25
- * Single port: 9141 for everything.
26
- * Development: Vite embedded as middleware (no separate server)
27
- * Production: Serves frontend static files + API
24
+ * Development: Elysia runs on port 9151, Vite dev server proxies /api and /ws from port 9141
25
+ * Production: Elysia runs on port 9141, serves static files from dist/ + API + WebSocket
28
26
  */
29
27
 
30
28
  function getLocalIps(): string[] {
@@ -48,8 +46,8 @@ const app = new Elysia()
48
46
  .use(errorHandlerMiddleware)
49
47
  .use(loggerMiddleware)
50
48
 
51
- // Health check endpoint
52
- .get('/health', () => ({
49
+ // API routes
50
+ .get('/api/health', () => ({
53
51
  status: 'ok',
54
52
  timestamp: new Date().toISOString(),
55
53
  environment: SERVER_ENV.NODE_ENV
@@ -58,50 +56,32 @@ const app = new Elysia()
58
56
  // Mount WebSocket router (all functionality now via WebSocket)
59
57
  .use(wsRouter.asPlugin('/ws'));
60
58
 
61
- if (isDevelopment) {
62
- // Development: Embed Vite as middleware no separate port
63
- // Uses Vite's direct APIs (transformIndexHtml, transformRequest) for speed,
64
- // with Node.js compat middleware only as fallback for edge cases.
65
- const { initViteDev, handleDevRequest, closeViteDev } = await import('./lib/vite-dev');
66
- const vite = await initViteDev();
59
+ if (!isDevelopment) {
60
+ // Production: serve static files from dist/ using @elysiajs/static
61
+ const { staticPlugin } = await import('@elysiajs/static');
67
62
 
68
- app.all('/*', async ({ request }) => handleDevRequest(vite, request));
63
+ app.use(staticPlugin({
64
+ assets: 'dist',
65
+ prefix: '/',
66
+ }));
69
67
 
70
- // Store cleanup function for graceful shutdown
71
- (globalThis as any).__closeViteDev = closeViteDev;
72
- } else {
73
- // Production: Read index.html once at startup as a string so Content-Length
74
- // is always correct. Bun.file() streaming can hang on some platforms.
68
+ // SPA fallback: serve index.html for any unmatched route (client-side routing)
75
69
  const distDir = resolve(process.cwd(), 'dist');
76
70
  const indexHtml = await Bun.file(resolve(distDir, 'index.html')).text();
77
71
 
78
- app.all('/*', ({ path }) => {
79
- // Serve static files from dist/
80
- if (path !== '/' && !path.includes('..')) {
81
- const filePath = resolve(distDir, path.slice(1));
82
- if (filePath.startsWith(distDir)) {
83
- try {
84
- if (statSync(filePath).isFile()) {
85
- return new Response(Bun.file(filePath));
86
- }
87
- } catch {}
88
- }
89
- }
90
-
91
- // SPA fallback: serve cached index.html
72
+ app.get('/*', () => {
92
73
  return new Response(indexHtml, {
93
74
  headers: { 'Content-Type': 'text/html; charset=utf-8' }
94
75
  });
95
76
  });
96
-
97
77
  }
98
78
 
99
79
  // Start server with proper initialization sequence
100
80
  async function startServer() {
101
- // Strict check: refuse to start if port is already in use
102
- if (await isPortInUse(PORT)) {
103
- console.error(`❌ Port ${PORT} is already in use. Please close the existing process first.`);
104
- process.exit(1);
81
+ // Find available port auto-increment if desired port is in use
82
+ const actualPort = await findAvailablePort(PORT);
83
+ if (actualPort !== PORT) {
84
+ debug.log('server', `⚠️ Port ${PORT} in use, using ${actualPort} instead`);
105
85
  }
106
86
 
107
87
  // Initialize database first before accepting connections
@@ -114,14 +94,14 @@ async function startServer() {
114
94
 
115
95
  // Start listening after database is ready
116
96
  app.listen({
117
- port: PORT,
97
+ port: actualPort,
118
98
  hostname: HOST
119
99
  }, () => {
120
- console.log(`🚀 Clopen running at http://localhost:${PORT}`);
100
+ console.log(`🚀 Clopen running at http://localhost:${actualPort}`);
121
101
  if (HOST === '0.0.0.0') {
122
102
  const ips = getLocalIps();
123
103
  for (const ip of ips) {
124
- console.log(`🌐 Network access: http://${ip}:${PORT}`);
104
+ console.log(`🌐 Network access: http://${ip}:${actualPort}`);
125
105
  }
126
106
  }
127
107
  });
@@ -136,10 +116,6 @@ startServer().catch((error) => {
136
116
  async function gracefulShutdown() {
137
117
  console.log('\n🛑 Shutting down server...');
138
118
  try {
139
- // Close Vite dev server if running
140
- if (isDevelopment && (globalThis as any).__closeViteDev) {
141
- await (globalThis as any).__closeViteDev();
142
- }
143
119
  // Dispose all AI engines
144
120
  await disposeAllEngines();
145
121
  // Stop accepting new connections
@@ -18,11 +18,18 @@ import { readFileSync } from 'fs';
18
18
  import { join } from 'path';
19
19
 
20
20
  // ── Server configuration (read once at import time) ─────────────────
21
+ const isDev = process.env.NODE_ENV !== 'production';
22
+
21
23
  export const SERVER_ENV = {
22
24
  NODE_ENV: (process.env.NODE_ENV || 'development') as string,
23
- PORT: process.env.PORT ? parseInt(process.env.PORT) : 9141,
25
+ /** Backend port — dev: PORT_BACKEND (default 9151), prod: PORT (default 9141) */
26
+ PORT: isDev
27
+ ? (process.env.PORT_BACKEND ? parseInt(process.env.PORT_BACKEND) : 9151)
28
+ : (process.env.PORT ? parseInt(process.env.PORT) : 9141),
29
+ /** Frontend port — only used in dev for Vite proxy coordination */
30
+ PORT_FRONTEND: process.env.PORT_FRONTEND ? parseInt(process.env.PORT_FRONTEND) : 9141,
24
31
  HOST: (process.env.HOST || 'localhost') as string,
25
- isDevelopment: process.env.NODE_ENV !== 'production',
32
+ isDevelopment: isDev,
26
33
  } as const;
27
34
 
28
35
  // ── .env parsing ────────────────────────────────────────────────────
@@ -23,3 +23,13 @@ export async function isPortInUse(port: number): Promise<boolean> {
23
23
  return false; // Connection refused = port is free
24
24
  }
25
25
  }
26
+
27
+ /** Find an available port starting from the given port, incrementing on collision */
28
+ export async function findAvailablePort(startPort: number, maxAttempts = 8): Promise<number> {
29
+ let port = startPort;
30
+ for (let i = 0; i < maxAttempts; i++) {
31
+ if (!(await isPortInUse(port))) return port;
32
+ port++;
33
+ }
34
+ throw new Error(`No available port found starting from ${startPort} (tried ${maxAttempts} ports)`);
35
+ }
package/bun.lock CHANGED
@@ -45,6 +45,7 @@
45
45
  "@types/bun": "^1.2.18",
46
46
  "@types/qrcode": "^1.5.6",
47
47
  "@types/xterm": "^3.0.0",
48
+ "concurrently": "^9.2.1",
48
49
  "eslint": "^9.31.0",
49
50
  "eslint-plugin-svelte": "^3.10.1",
50
51
  "globals": "^16.3.0",
@@ -488,7 +489,7 @@
488
489
 
489
490
  "chromium-bidi": ["chromium-bidi@11.0.0", "", { "dependencies": { "mitt": "^3.0.1", "zod": "^3.24.1" }, "peerDependencies": { "devtools-protocol": "*" } }, "sha512-cM3DI+OOb89T3wO8cpPSro80Q9eKYJ7hGVXoGS3GkDPxnYSqiv+6xwpIf6XERyJ9Tdsl09hmNmY94BkgZdVekw=="],
490
491
 
491
- "cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="],
492
+ "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
492
493
 
493
494
  "cloudflared": ["cloudflared@0.7.1", "", { "bin": { "cloudflared": "lib/cloudflared.js" } }, "sha512-jJn1Gu9Tf4qnIu8tfiHZ25Hs8rNcRYSVf8zAd97wvYdOCzftm1CTs1S/RPhijjGi8gUT1p9yzfDi9zYlU/0RwA=="],
494
495
 
@@ -506,6 +507,8 @@
506
507
 
507
508
  "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
508
509
 
510
+ "concurrently": ["concurrently@9.2.1", "", { "dependencies": { "chalk": "4.1.2", "rxjs": "7.8.2", "shell-quote": "1.8.3", "supports-color": "8.1.1", "tree-kill": "1.2.2", "yargs": "17.7.2" }, "bin": { "conc": "dist/bin/concurrently.js", "concurrently": "dist/bin/concurrently.js" } }, "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng=="],
511
+
509
512
  "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="],
510
513
 
511
514
  "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
@@ -1008,6 +1011,8 @@
1008
1011
 
1009
1012
  "rx.mini": ["rx.mini@1.4.0", "", {}, "sha512-8w5cSc1mwNja7fl465DXOkVvIOkpvh2GW4jo31nAIvX4WTXCsRnKJGUfiDBzWtYRInEcHAUYIZfzusjIrea8gA=="],
1010
1013
 
1014
+ "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="],
1015
+
1011
1016
  "sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="],
1012
1017
 
1013
1018
  "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
@@ -1030,6 +1035,8 @@
1030
1035
 
1031
1036
  "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
1032
1037
 
1038
+ "shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="],
1039
+
1033
1040
  "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
1034
1041
 
1035
1042
  "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
@@ -1070,7 +1077,7 @@
1070
1077
 
1071
1078
  "strtok3": ["strtok3@10.3.4", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg=="],
1072
1079
 
1073
- "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
1080
+ "supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
1074
1081
 
1075
1082
  "svelte": ["svelte@5.36.12", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "esm-env": "^1.2.1", "esrap": "^2.1.0", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-c3mWT+b0yBLl3gPGSHiy4pdSQCsPNTjLC0tVoOhrGJ6PPfCzD/RQpAmAfJtQZ304CAae2ph+L3C4aqds3R3seQ=="],
1076
1083
 
@@ -1102,6 +1109,8 @@
1102
1109
 
1103
1110
  "token-types": ["token-types@6.0.4", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-MD9MjpVNhVyH4fyd5rKphjvt/1qj+PtQUz65aFqAZA6XniWAuSFRjLk3e2VALEFlh9OwBpXUN7rfeqSnT/Fmkw=="],
1104
1111
 
1112
+ "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="],
1113
+
1105
1114
  "ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="],
1106
1115
 
1107
1116
  "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
@@ -1162,7 +1171,7 @@
1162
1171
 
1163
1172
  "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
1164
1173
 
1165
- "wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
1174
+ "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
1166
1175
 
1167
1176
  "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
1168
1177
 
@@ -1170,15 +1179,15 @@
1170
1179
 
1171
1180
  "xterm": ["xterm@5.3.0", "", {}, "sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg=="],
1172
1181
 
1173
- "y18n": ["y18n@4.0.3", "", {}, "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="],
1182
+ "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
1174
1183
 
1175
1184
  "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
1176
1185
 
1177
1186
  "yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="],
1178
1187
 
1179
- "yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="],
1188
+ "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
1180
1189
 
1181
- "yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="],
1190
+ "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
1182
1191
 
1183
1192
  "yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="],
1184
1193
 
@@ -1218,8 +1227,6 @@
1218
1227
 
1219
1228
  "@puppeteer/browsers/tar-fs": ["tar-fs@3.1.1", "", { "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" }, "optionalDependencies": { "bare-fs": "^4.0.1", "bare-path": "^3.0.0" } }, "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg=="],
1220
1229
 
1221
- "@puppeteer/browsers/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
1222
-
1223
1230
  "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.5", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.4", "tslib": "^2.4.0" }, "bundled": true }, "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q=="],
1224
1231
 
1225
1232
  "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.4.5", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg=="],
@@ -1250,6 +1257,8 @@
1250
1257
 
1251
1258
  "body-parser/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
1252
1259
 
1260
+ "chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
1261
+
1253
1262
  "eslint/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
1254
1263
 
1255
1264
  "express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
@@ -1280,6 +1289,8 @@
1280
1289
 
1281
1290
  "puppeteer-core/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
1282
1291
 
1292
+ "qrcode/yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="],
1293
+
1283
1294
  "rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="],
1284
1295
 
1285
1296
  "router/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
@@ -1322,32 +1333,32 @@
1322
1333
 
1323
1334
  "werift-ice/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
1324
1335
 
1325
- "yargs/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="],
1326
-
1327
1336
  "yauzl/buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="],
1328
1337
 
1329
1338
  "@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
1330
1339
 
1331
1340
  "@puppeteer/browsers/tar-fs/tar-stream": ["tar-stream@3.1.7", "", { "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ=="],
1332
1341
 
1333
- "@puppeteer/browsers/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
1334
-
1335
- "@puppeteer/browsers/yargs/y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
1336
-
1337
- "@puppeteer/browsers/yargs/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
1338
-
1339
1342
  "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ=="],
1340
1343
 
1341
1344
  "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
1342
1345
 
1343
1346
  "ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
1344
1347
 
1345
- "yargs/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
1348
+ "qrcode/yargs/cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="],
1349
+
1350
+ "qrcode/yargs/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="],
1351
+
1352
+ "qrcode/yargs/y18n": ["y18n@4.0.3", "", {}, "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="],
1353
+
1354
+ "qrcode/yargs/yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="],
1355
+
1356
+ "qrcode/yargs/cliui/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
1346
1357
 
1347
- "@puppeteer/browsers/yargs/cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
1358
+ "qrcode/yargs/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
1348
1359
 
1349
- "yargs/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
1360
+ "qrcode/yargs/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
1350
1361
 
1351
- "yargs/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
1362
+ "qrcode/yargs/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
1352
1363
  }
1353
1364
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myrialabs/clopen",
3
- "version": "0.0.6",
3
+ "version": "0.0.7",
4
4
  "description": "All-in-one web workspace for Claude Code & OpenCode — chat, terminal, git, browser preview, checkpoints, and real-time collaboration",
5
5
  "author": "Myria Labs",
6
6
  "license": "MIT",
@@ -45,9 +45,11 @@
45
45
  "bun": ">=1.2.12"
46
46
  },
47
47
  "scripts": {
48
- "dev": "bun --watch backend/index.ts",
48
+ "dev": "bun scripts/dev.ts",
49
+ "dev:backend": "bun --watch backend/index.ts",
50
+ "dev:frontend": "bunx vite dev",
49
51
  "build": "vite build",
50
- "start": "bun backend/index.ts",
52
+ "start": "NODE_ENV=production bun backend/index.ts",
51
53
  "check": "svelte-check --tsconfig ./tsconfig.json",
52
54
  "lint": "eslint .",
53
55
  "lint:fix": "eslint . --fix",
@@ -60,6 +62,7 @@
60
62
  "@types/bun": "^1.2.18",
61
63
  "@types/qrcode": "^1.5.6",
62
64
  "@types/xterm": "^3.0.0",
65
+ "concurrently": "^9.2.1",
63
66
  "eslint": "^9.31.0",
64
67
  "eslint-plugin-svelte": "^3.10.1",
65
68
  "globals": "^16.3.0",
package/scripts/dev.ts ADDED
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env bun
2
+
3
+ /**
4
+ * Development script — runs backend and frontend concurrently.
5
+ * Resolves available ports before spawning, then passes them via env
6
+ * so Vite proxy and Elysia backend use the same coordinated ports.
7
+ */
8
+
9
+ import concurrently from 'concurrently';
10
+ import { findAvailablePort } from '../backend/lib/shared/port-utils';
11
+
12
+ const desiredBackend = process.env.PORT_BACKEND ? parseInt(process.env.PORT_BACKEND) : 9151;
13
+ const desiredFrontend = process.env.PORT_FRONTEND ? parseInt(process.env.PORT_FRONTEND) : 9141;
14
+
15
+ // Resolve available ports
16
+ const backendPort = await findAvailablePort(desiredBackend);
17
+ let frontendPort = await findAvailablePort(desiredFrontend);
18
+
19
+ // Ensure they don't collide
20
+ if (frontendPort === backendPort) {
21
+ frontendPort = await findAvailablePort(frontendPort + 1);
22
+ }
23
+
24
+ if (backendPort !== desiredBackend) {
25
+ console.log(`⚠️ Backend port ${desiredBackend} in use, using ${backendPort}`);
26
+ }
27
+ if (frontendPort !== desiredFrontend) {
28
+ console.log(`⚠️ Frontend port ${desiredFrontend} in use, using ${frontendPort}`);
29
+ }
30
+
31
+ console.log(`Backend: http://localhost:${backendPort}`);
32
+ console.log(`Frontend: http://localhost:${frontendPort}`);
33
+ console.log();
34
+
35
+ const portEnv = {
36
+ PORT_BACKEND: String(backendPort),
37
+ PORT_FRONTEND: String(frontendPort),
38
+ };
39
+
40
+ concurrently([
41
+ { command: 'bun --watch backend/index.ts', name: 'backend', prefixColor: 'blue', env: portEnv },
42
+ { command: 'bunx vite dev', name: 'frontend', prefixColor: 'green', env: portEnv },
43
+ ], {
44
+ killOthersOn: ['failure'],
45
+ });
package/vite.config.ts CHANGED
@@ -3,9 +3,26 @@ import { defineConfig } from 'vite';
3
3
  import tailwindcss from '@tailwindcss/vite';
4
4
  import { resolve } from 'path';
5
5
 
6
+ const frontendPort = parseInt(process.env.PORT_FRONTEND || '9141');
7
+ const backendPort = parseInt(process.env.PORT_BACKEND || '9151');
8
+
6
9
  export default defineConfig({
7
10
  plugins: [tailwindcss(), svelte()],
8
11
  publicDir: 'static',
12
+ server: {
13
+ port: frontendPort,
14
+ strictPort: false,
15
+ proxy: {
16
+ '/api': {
17
+ target: `http://localhost:${backendPort}`,
18
+ changeOrigin: true,
19
+ },
20
+ '/ws': {
21
+ target: `ws://localhost:${backendPort}`,
22
+ ws: true,
23
+ },
24
+ },
25
+ },
9
26
  build: {
10
27
  outDir: 'dist',
11
28
  emptyOutDir: true,
@@ -1,295 +0,0 @@
1
- /**
2
- * Vite Dev Server Integration
3
- *
4
- * Embeds Vite as middleware inside the Elysia/Bun server.
5
- * Uses Vite's direct APIs (transformIndexHtml, transformRequest) for speed,
6
- * with Node.js compat middleware adapter only for Vite internals (HMR, pre-bundled deps).
7
- */
8
-
9
- import { createServer as createViteServer, type ViteDevServer, type Connect } from 'vite';
10
- import { IncomingMessage, ServerResponse } from 'node:http';
11
- import { resolve } from 'node:path';
12
- import { statSync } from 'node:fs';
13
-
14
- let vite: ViteDevServer | null = null;
15
-
16
- const PUBLIC_DIR = resolve(process.cwd(), 'static');
17
- const INDEX_PATH = resolve(process.cwd(), 'index.html');
18
-
19
- // Safety timeout for the Node.js compat middleware adapter (ms).
20
- // Prevents hanging if Vite middleware never calls res.end() or next().
21
- const MIDDLEWARE_TIMEOUT = 10000;
22
-
23
- // ============================================================================
24
- // Lifecycle
25
- // ============================================================================
26
-
27
- export async function initViteDev(): Promise<ViteDevServer> {
28
- vite = await createViteServer({
29
- configFile: './vite.config.ts',
30
- server: { middlewareMode: true },
31
- appType: 'spa'
32
- });
33
-
34
- return vite;
35
- }
36
-
37
- export function getViteDev(): ViteDevServer | null {
38
- return vite;
39
- }
40
-
41
- export async function closeViteDev(): Promise<void> {
42
- if (vite) {
43
- await vite.close();
44
- vite = null;
45
- }
46
- }
47
-
48
- // ============================================================================
49
- // Main Request Handler
50
- // ============================================================================
51
-
52
- export async function handleDevRequest(viteServer: ViteDevServer, request: Request): Promise<Response> {
53
- const url = new URL(request.url);
54
- const pathname = url.pathname;
55
-
56
- // 1. Static public files
57
- const publicFile = servePublicFile(pathname);
58
- if (publicFile) return publicFile;
59
-
60
- // 2. HTML / SPA routes
61
- if (isHtmlRequest(pathname)) {
62
- return serveHtml(viteServer, pathname);
63
- }
64
-
65
- // 3. Module requests (skip Vite internals like /__vite_hmr)
66
- if (!pathname.startsWith('/__')) {
67
- const moduleResponse = await serveModule(viteServer, pathname + url.search, request);
68
- if (moduleResponse) return moduleResponse;
69
- }
70
-
71
- // 4. Fallback: Vite connect middleware (for HMR, pre-bundled deps, etc.)
72
- const middlewareResponse = await pipeViteMiddleware(viteServer.middlewares, request);
73
- if (middlewareResponse) return middlewareResponse;
74
-
75
- return new Response('Not Found', { status: 404 });
76
- }
77
-
78
- // ============================================================================
79
- // Static Public Files
80
- // ============================================================================
81
-
82
- function servePublicFile(pathname: string): Response | null {
83
- if (pathname === '/' || pathname.includes('..')) return null;
84
-
85
- const filePath = resolve(PUBLIC_DIR, pathname.slice(1));
86
- if (!filePath.startsWith(PUBLIC_DIR)) return null;
87
-
88
- // Use statSync to verify the path is a regular file, not a directory.
89
- // Bun.file().exists() returns inconsistent results for directories across
90
- // platforms (Linux/macOS/Windows) and Bun versions, which can cause
91
- // Response(Bun.file(directory)) to hang indefinitely.
92
- try {
93
- if (!statSync(filePath).isFile()) return null;
94
- } catch {
95
- return null;
96
- }
97
-
98
- const file = Bun.file(filePath);
99
- return new Response(file, {
100
- headers: {
101
- 'Content-Type': file.type || 'application/octet-stream',
102
- 'Cache-Control': 'public, max-age=3600'
103
- }
104
- });
105
- }
106
-
107
- // ============================================================================
108
- // HTML / SPA
109
- // ============================================================================
110
-
111
- function isHtmlRequest(pathname: string): boolean {
112
- if (pathname === '/') return true;
113
- if (pathname.startsWith('/@') || pathname.startsWith('/__')) return false;
114
- if (pathname.startsWith('/node_modules/')) return false;
115
- const lastSegment = pathname.split('/').pop() || '';
116
- return !lastSegment.includes('.');
117
- }
118
-
119
- async function serveHtml(viteServer: ViteDevServer, pathname: string): Promise<Response> {
120
- const rawHtml = await Bun.file(INDEX_PATH).text();
121
- const html = await viteServer.transformIndexHtml(pathname, rawHtml);
122
- return new Response(html, {
123
- headers: { 'Content-Type': 'text/html; charset=utf-8' }
124
- });
125
- }
126
-
127
- // ============================================================================
128
- // Module Requests
129
- // ============================================================================
130
-
131
- function getModuleContentType(url: string): string {
132
- const pathname = url.split('?')[0];
133
- // Raw CSS only when explicitly requested with ?direct (e.g. <link> tags).
134
- // CSS imported via JS is transformed to a JS module by Vite (for HMR).
135
- if (pathname.endsWith('.css') && url.includes('direct')) {
136
- return 'text/css';
137
- }
138
- return 'application/javascript';
139
- }
140
-
141
- async function serveModule(viteServer: ViteDevServer, url: string, request: Request): Promise<Response | null> {
142
- try {
143
- const result = await viteServer.transformRequest(url);
144
- if (!result) return null;
145
-
146
- if (result.etag) {
147
- const ifNoneMatch = request.headers.get('if-none-match');
148
- if (ifNoneMatch === result.etag) {
149
- return new Response(null, { status: 304 });
150
- }
151
- }
152
-
153
- const headers: Record<string, string> = {
154
- 'Content-Type': getModuleContentType(url),
155
- };
156
- if (result.etag) {
157
- headers['ETag'] = result.etag;
158
- }
159
-
160
- return new Response(result.code, { headers });
161
- } catch {
162
- return null;
163
- }
164
- }
165
-
166
- // ============================================================================
167
- // Vite Connect Middleware Adapter
168
- // ============================================================================
169
-
170
- const MOCK_SOCKET = {
171
- remoteAddress: '127.0.0.1',
172
- remotePort: 0,
173
- remoteFamily: 'IPv4',
174
- encrypted: false,
175
- writable: true,
176
- readable: true,
177
- destroy() {},
178
- end() {},
179
- write() { return true; },
180
- on() { return this; },
181
- once() { return this; },
182
- off() { return this; },
183
- emit() { return false; },
184
- addListener() { return this; },
185
- removeListener() { return this; },
186
- setTimeout() { return this; },
187
- setNoDelay() { return this; },
188
- setKeepAlive() { return this; },
189
- ref() { return this; },
190
- unref() { return this; },
191
- address() { return { address: '127.0.0.1', family: 'IPv4', port: 0 }; }
192
- };
193
-
194
- /**
195
- * Bridges Web API Request → Node.js IncomingMessage/ServerResponse → Web API Response.
196
- * Required because Vite's connect middleware uses the Node.js HTTP API.
197
- */
198
- function pipeViteMiddleware(
199
- middleware: Connect.Server,
200
- request: Request
201
- ): Promise<Response | null> {
202
- return new Promise((resolve) => {
203
- const url = new URL(request.url);
204
-
205
- const req = new IncomingMessage(MOCK_SOCKET as any);
206
- req.method = request.method;
207
- req.url = url.pathname + url.search;
208
- req.headers = request.headers.toJSON();
209
- req.push(null);
210
-
211
- const res = new ServerResponse(req);
212
- let ended = false;
213
- const chunks: Uint8Array[] = [];
214
-
215
- const safetyTimer = setTimeout(() => {
216
- if (!ended) {
217
- ended = true;
218
- resolve(null);
219
- }
220
- }, MIDDLEWARE_TIMEOUT);
221
-
222
- function finalize() {
223
- if (ended) return;
224
- ended = true;
225
- clearTimeout(safetyTimer);
226
-
227
- const nodeHeaders = res.getHeaders();
228
- const h = new Headers();
229
- for (const key in nodeHeaders) {
230
- // Skip hop-by-hop headers — Bun sets correct Content-Length/Transfer-Encoding
231
- // based on the actual body. Forwarding stale values from ServerResponse
232
- // causes browser to wait for data that never arrives.
233
- const lower = key.toLowerCase();
234
- if (lower === 'transfer-encoding' || lower === 'content-length' ||
235
- lower === 'connection' || lower === 'keep-alive') continue;
236
-
237
- const v = nodeHeaders[key];
238
- if (v === undefined) continue;
239
- if (Array.isArray(v)) {
240
- for (let i = 0; i < v.length; i++) h.append(key, v[i]);
241
- } else {
242
- h.set(key, String(v));
243
- }
244
- }
245
-
246
- const body = chunks.length === 0
247
- ? null
248
- : Buffer.concat(chunks);
249
-
250
- resolve(new Response(body, { status: res.statusCode, headers: h }));
251
- }
252
-
253
- res.write = function (chunk: any, encodingOrCb?: any, cb?: any): boolean {
254
- if (chunk != null) {
255
- chunks.push(
256
- typeof chunk === 'string'
257
- ? Buffer.from(chunk, typeof encodingOrCb === 'string' ? encodingOrCb as BufferEncoding : 'utf-8')
258
- : chunk instanceof Uint8Array ? chunk : Buffer.from(chunk)
259
- );
260
- }
261
- const callback = typeof encodingOrCb === 'function' ? encodingOrCb : cb;
262
- if (typeof callback === 'function') callback();
263
- return true;
264
- };
265
-
266
- res.end = function (data?: any, encodingOrCb?: any, cb?: any): ServerResponse {
267
- if (ended) return res;
268
-
269
- if (typeof data === 'function') {
270
- data();
271
- } else {
272
- if (data != null) {
273
- chunks.push(
274
- typeof data === 'string'
275
- ? Buffer.from(data, typeof encodingOrCb === 'string' ? encodingOrCb as BufferEncoding : 'utf-8')
276
- : data instanceof Uint8Array ? data : Buffer.from(data)
277
- );
278
- }
279
- const callback = typeof encodingOrCb === 'function' ? encodingOrCb : cb;
280
- if (typeof callback === 'function') callback();
281
- }
282
-
283
- finalize();
284
- return res;
285
- } as any;
286
-
287
- middleware(req as any, res as any, () => {
288
- if (!ended) {
289
- ended = true;
290
- clearTimeout(safetyTimer);
291
- resolve(null);
292
- }
293
- });
294
- });
295
- }