@myrialabs/clopen 0.0.2 → 0.0.4

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,49 +1,25 @@
1
1
  /**
2
- * Vite Dev Server Integration (Bun-optimized)
2
+ * Vite Dev Server Integration
3
3
  *
4
4
  * Embeds Vite as middleware inside the Elysia/Bun server.
5
- *
6
- * Performance optimizations:
7
- * - HTML requests use vite.transformIndexHtml() directly (bypass Node compat layer)
8
- * - Module requests use vite.transformRequest() directly (bypass Node compat layer)
9
- * - Only edge cases (Vite internals, pre-bundled deps) go through middleware adapter
10
- *
11
- * Reliability:
12
- * - All async operations have timeouts to prevent hanging promises
13
- * - HTML has raw fallback if Vite transform hangs
14
- * - Middleware adapter has safety timeout
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).
15
7
  */
16
8
 
17
9
  import { createServer as createViteServer, type ViteDevServer, type Connect } from 'vite';
18
10
  import { IncomingMessage, ServerResponse } from 'node:http';
19
11
  import { resolve } from 'node:path';
12
+ import { statSync } from 'node:fs';
20
13
 
21
14
  let vite: ViteDevServer | null = null;
22
15
 
23
- // Resolved paths (computed once at startup)
24
16
  const PUBLIC_DIR = resolve(process.cwd(), 'static');
25
17
  const INDEX_PATH = resolve(process.cwd(), 'index.html');
26
18
 
27
- // Timeouts (ms)
28
- const HTML_TRANSFORM_TIMEOUT = 5000;
29
- const MODULE_TRANSFORM_TIMEOUT = 10000;
19
+ // Safety timeout for the Node.js compat middleware adapter (ms).
20
+ // Prevents hanging if Vite middleware never calls res.end() or next().
30
21
  const MIDDLEWARE_TIMEOUT = 10000;
31
22
 
32
- // ============================================================================
33
- // Utilities
34
- // ============================================================================
35
-
36
- /**
37
- * Race a promise against a timeout. Returns null on timeout (never rejects).
38
- */
39
- function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T | null> {
40
- let timer: ReturnType<typeof setTimeout>;
41
- return Promise.race([
42
- promise,
43
- new Promise<null>((resolve) => { timer = setTimeout(() => resolve(null), ms); })
44
- ]).finally(() => clearTimeout(timer));
45
- }
46
-
47
23
  // ============================================================================
48
24
  // Lifecycle
49
25
  // ============================================================================
@@ -73,45 +49,26 @@ export async function closeViteDev(): Promise<void> {
73
49
  // Main Request Handler
74
50
  // ============================================================================
75
51
 
76
- /**
77
- * Handle an HTTP request in dev mode.
78
- * Uses Vite's direct APIs for speed, middleware only as fallback.
79
- * All paths have timeouts to guarantee a response — never hangs.
80
- */
81
52
  export async function handleDevRequest(viteServer: ViteDevServer, request: Request): Promise<Response> {
82
53
  const url = new URL(request.url);
83
54
  const pathname = url.pathname;
84
55
 
85
- // 1. Static public files — fast Bun.file() (no Vite overhead)
86
- const publicFile = await servePublicFile(pathname);
56
+ // 1. Static public files
57
+ const publicFile = servePublicFile(pathname);
87
58
  if (publicFile) return publicFile;
88
59
 
89
- // 2. HTML / SPA routes — direct vite.transformIndexHtml() with timeout + fallback
60
+ // 2. HTML / SPA routes
90
61
  if (isHtmlRequest(pathname)) {
91
- const htmlResponse = await withTimeout(serveHtml(viteServer, pathname), HTML_TRANSFORM_TIMEOUT);
92
- if (htmlResponse) return htmlResponse;
93
-
94
- // Fallback: serve raw HTML without Vite transforms (HMR won't work but page loads)
95
- try {
96
- const rawHtml = await Bun.file(INDEX_PATH).text();
97
- return new Response(rawHtml, {
98
- headers: { 'Content-Type': 'text/html; charset=utf-8' }
99
- });
100
- } catch {
101
- // INDEX_PATH doesn't exist — fall through
102
- }
62
+ return serveHtml(viteServer, pathname);
103
63
  }
104
64
 
105
- // 3. Module requests direct vite.transformRequest() with timeout
65
+ // 3. Module requests (skip Vite internals like /__vite_hmr)
106
66
  if (!pathname.startsWith('/__')) {
107
- const moduleResponse = await withTimeout(
108
- serveModule(viteServer, pathname + url.search, request),
109
- MODULE_TRANSFORM_TIMEOUT
110
- );
67
+ const moduleResponse = await serveModule(viteServer, pathname + url.search, request);
111
68
  if (moduleResponse) return moduleResponse;
112
69
  }
113
70
 
114
- // 4. Fallback: pipe through Vite's connect middleware (with safety timeout)
71
+ // 4. Fallback: Vite connect middleware (for HMR, pre-bundled deps, etc.)
115
72
  const middlewareResponse = await pipeViteMiddleware(viteServer.middlewares, request);
116
73
  if (middlewareResponse) return middlewareResponse;
117
74
 
@@ -119,23 +76,26 @@ export async function handleDevRequest(viteServer: ViteDevServer, request: Reque
119
76
  }
120
77
 
121
78
  // ============================================================================
122
- // Fast Path: Public Files
79
+ // Static Public Files
123
80
  // ============================================================================
124
81
 
125
- /**
126
- * Try serving a static file from the public directory (static/).
127
- * Uses BunFile directly (not .stream()) so Bun sets Content-Length automatically,
128
- * preventing endless loading when the browser can't detect stream end.
129
- */
130
- async function servePublicFile(pathname: string): Promise<Response | null> {
131
- if (pathname.includes('..')) return null;
82
+ function servePublicFile(pathname: string): Response | null {
83
+ if (pathname === '/' || pathname.includes('..')) return null;
132
84
 
133
85
  const filePath = resolve(PUBLIC_DIR, pathname.slice(1));
134
86
  if (!filePath.startsWith(PUBLIC_DIR)) return null;
135
87
 
136
- const file = Bun.file(filePath);
137
- if (!(await file.exists())) return null;
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
+ }
138
97
 
98
+ const file = Bun.file(filePath);
139
99
  return new Response(file, {
140
100
  headers: {
141
101
  'Content-Type': file.type || 'application/octet-stream',
@@ -145,12 +105,9 @@ async function servePublicFile(pathname: string): Promise<Response | null> {
145
105
  }
146
106
 
147
107
  // ============================================================================
148
- // Fast Path: HTML / SPA
108
+ // HTML / SPA
149
109
  // ============================================================================
150
110
 
151
- /**
152
- * Check if a request is for an HTML page (root or SPA client-side route).
153
- */
154
111
  function isHtmlRequest(pathname: string): boolean {
155
112
  if (pathname === '/') return true;
156
113
  if (pathname.startsWith('/@') || pathname.startsWith('/__')) return false;
@@ -159,9 +116,6 @@ function isHtmlRequest(pathname: string): boolean {
159
116
  return !lastSegment.includes('.');
160
117
  }
161
118
 
162
- /**
163
- * Serve transformed HTML directly via Vite's API.
164
- */
165
119
  async function serveHtml(viteServer: ViteDevServer, pathname: string): Promise<Response> {
166
120
  const rawHtml = await Bun.file(INDEX_PATH).text();
167
121
  const html = await viteServer.transformIndexHtml(pathname, rawHtml);
@@ -171,23 +125,19 @@ async function serveHtml(viteServer: ViteDevServer, pathname: string): Promise<R
171
125
  }
172
126
 
173
127
  // ============================================================================
174
- // Fast Path: Module Requests
128
+ // Module Requests
175
129
  // ============================================================================
176
130
 
177
131
  function getModuleContentType(url: string): string {
178
132
  const pathname = url.split('?')[0];
179
- // Only serve as raw CSS when explicitly requested with ?direct (e.g. <link> tags).
180
- // CSS imported via JS is always transformed to a JavaScript module by Vite
181
- // (for HMR / style injection), so it must be served as application/javascript.
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).
182
135
  if (pathname.endsWith('.css') && url.includes('direct')) {
183
136
  return 'text/css';
184
137
  }
185
138
  return 'application/javascript';
186
139
  }
187
140
 
188
- /**
189
- * Try to serve a module using Vite's transformRequest API.
190
- */
191
141
  async function serveModule(viteServer: ViteDevServer, url: string, request: Request): Promise<Response | null> {
192
142
  try {
193
143
  const result = await viteServer.transformRequest(url);
@@ -214,7 +164,7 @@ async function serveModule(viteServer: ViteDevServer, url: string, request: Requ
214
164
  }
215
165
 
216
166
  // ============================================================================
217
- // Fallback: Vite Connect Middleware Adapter (with safety timeout)
167
+ // Vite Connect Middleware Adapter
218
168
  // ============================================================================
219
169
 
220
170
  const MOCK_SOCKET = {
@@ -242,9 +192,8 @@ const MOCK_SOCKET = {
242
192
  };
243
193
 
244
194
  /**
245
- * Pipe a request through Vite's connect middleware.
246
- * Has a safety timeout to prevent promises that never resolve
247
- * (e.g., middleware errors without calling res.end() or next()).
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.
248
197
  */
249
198
  function pipeViteMiddleware(
250
199
  middleware: Connect.Server,
@@ -263,8 +212,6 @@ function pipeViteMiddleware(
263
212
  let ended = false;
264
213
  const chunks: Uint8Array[] = [];
265
214
 
266
- // Safety timeout — if middleware never calls res.end() or next(),
267
- // resolve with null to prevent hanging the HTTP response forever.
268
215
  const safetyTimer = setTimeout(() => {
269
216
  if (!ended) {
270
217
  ended = true;
@@ -280,12 +227,9 @@ function pipeViteMiddleware(
280
227
  const nodeHeaders = res.getHeaders();
281
228
  const h = new Headers();
282
229
  for (const key in nodeHeaders) {
283
- // Skip hop-by-hop headers — these are transport-level headers that
284
- // must NOT be forwarded through the adapter. Bun will set the correct
285
- // Content-Length and Transfer-Encoding based on the actual body.
286
- // Forwarding stale Content-Length from the Node.js ServerResponse
287
- // can cause a mismatch with the collected body, making the browser
288
- // wait for more data that never arrives (infinite loading spinner).
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.
289
233
  const lower = key.toLowerCase();
290
234
  if (lower === 'transfer-encoding' || lower === 'content-length' ||
291
235
  lower === 'connection' || lower === 'keep-alive') continue;
@@ -301,9 +245,7 @@ function pipeViteMiddleware(
301
245
 
302
246
  const body = chunks.length === 0
303
247
  ? null
304
- : chunks.length === 1
305
- ? chunks[0]
306
- : Buffer.concat(chunks);
248
+ : Buffer.concat(chunks);
307
249
 
308
250
  resolve(new Response(body, { status: res.statusCode, headers: h }));
309
251
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myrialabs/clopen",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
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",
package/vite.config.ts CHANGED
@@ -4,7 +4,7 @@ import tailwindcss from '@tailwindcss/vite';
4
4
  import { resolve } from 'path';
5
5
 
6
6
  export default defineConfig({
7
- plugins: [svelte(), tailwindcss()],
7
+ plugins: [tailwindcss(), svelte()],
8
8
  publicDir: 'static',
9
9
  build: {
10
10
  outDir: 'dist',