@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 +7 -1
- package/backend/index.ts +21 -45
- package/backend/lib/shared/env.ts +9 -2
- package/backend/lib/shared/port-utils.ts +10 -0
- package/bun.lock +31 -20
- package/package.json +6 -3
- package/scripts/dev.ts +45 -0
- package/vite.config.ts +17 -0
- package/backend/lib/vite-dev.ts +0 -295
package/.env.example
CHANGED
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 {
|
|
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
|
-
*
|
|
26
|
-
*
|
|
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
|
-
//
|
|
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
|
-
//
|
|
63
|
-
|
|
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.
|
|
63
|
+
app.use(staticPlugin({
|
|
64
|
+
assets: 'dist',
|
|
65
|
+
prefix: '/',
|
|
66
|
+
}));
|
|
69
67
|
|
|
70
|
-
//
|
|
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.
|
|
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
|
-
//
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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:
|
|
97
|
+
port: actualPort,
|
|
118
98
|
hostname: HOST
|
|
119
99
|
}, () => {
|
|
120
|
-
console.log(`🚀 Clopen running at http://localhost:${
|
|
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}:${
|
|
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
|
-
|
|
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:
|
|
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@
|
|
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@
|
|
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@
|
|
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@
|
|
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@
|
|
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@
|
|
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/
|
|
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
|
-
"
|
|
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.
|
|
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
|
|
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,
|
package/backend/lib/vite-dev.ts
DELETED
|
@@ -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
|
-
}
|