flashts 1.0.6 → 1.0.8

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/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  All notable changes to FlashTS will be documented in this file.
4
4
 
5
+ ## [1.0.8] - 2026-01-25
6
+
7
+ ### Fixed
8
+
9
+ - **Production Vite Reliability**: Optimized Vite execution and path resolution for production environments.
10
+ - **ENAMETOOLONG Issue**: Relocated the temporary runtime environment from the package directory to the system temp folder (`os.tmpdir()`). This resolves Windows path length limitations.
11
+ - **CLI Exit Responsiveness**: Optimized SIGINT/SIGTERM handling to ensure the CLI and all subprocesses close instantly.
12
+
13
+ ## [1.0.7] - 2026-01-25
14
+
15
+ ### Fixed
16
+
17
+ - **Vite Preview Timeout**: Resolved the issue where the live preview would hang on "launching" in production by moving `vite` and `@vitejs/plugin-react` to core dependencies and using direct execution.
18
+ - **CLI Exit Responsiveness**: Optimized SIGINT/SIGTERM handling to ensure the CLI and all subprocesses close instantly with a single Ctrl+C, eliminating the previous 500ms delay.
19
+
5
20
  ## [1.0.6] - 2026-01-24
6
21
 
7
22
  ### Added
package/bin/cli.ts CHANGED
@@ -10,7 +10,7 @@ const program = new Command();
10
10
  program
11
11
  .name("flashts")
12
12
  .description("FlashTS: High-performance TypeScript Playground CLI")
13
- .version("1.0.6")
13
+ .version("1.0.8")
14
14
  .option("-p, --port <number>", "Port to run the server on", "3000")
15
15
  .option("-s, --share", "Generate a shareable public link")
16
16
  .option("--no-open", "Do not open the browser automatically")
@@ -45,7 +45,7 @@ program
45
45
 
46
46
  const packageRoot = join(import.meta.dir, "..");
47
47
 
48
- console.log(`\n⚡ ${pc.bold(pc.cyan("FlashTS"))} ${pc.dim("v1.0.6")}`);
48
+ console.log(`\n⚡ ${pc.bold(pc.cyan("FlashTS"))} ${pc.dim("v1.0.8")}`);
49
49
  console.log(`${pc.green("➜")} Local: ${pc.cyan(`http://localhost:${port}`)}`);
50
50
 
51
51
  // Start the server process
@@ -80,13 +80,16 @@ program
80
80
  }, 1000);
81
81
  }
82
82
 
83
- // Keep process alive and ensure total cleanup
84
- process.on("SIGINT", () => {
85
- console.log(pc.yellow("\nStopping FlashTS..."));
86
- // The server itself will handle killing vites on SIGINT
87
- serverProc.kill("SIGINT");
88
- setTimeout(() => process.exit(0), 500);
89
- });
83
+ const handleExit = () => {
84
+ // Don't log anything else after this to keep terminal clean
85
+ try {
86
+ serverProc.kill();
87
+ } catch (e) {}
88
+ process.exit(0);
89
+ };
90
+
91
+ process.on("SIGINT", handleExit);
92
+ process.on("SIGTERM", handleExit);
90
93
  });
91
94
 
92
95
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flashts",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "High-performance TypeScript/JavaScript Playground powered by Bun",
5
5
  "main": "server/index.ts",
6
6
  "bin": {
@@ -43,14 +43,15 @@
43
43
  "react": "^19.2.3",
44
44
  "react-dom": "^19.2.0",
45
45
  "react-resizable-panels": "^4.4.1",
46
- "tailwind-merge": "^3.4.0"
46
+ "tailwind-merge": "^3.4.0",
47
+ "vite": "npm:rolldown-vite@7.2.5",
48
+ "@vitejs/plugin-react": "^5.1.1"
47
49
  },
48
50
  "devDependencies": {
49
51
  "@eslint/js": "^9.39.1",
50
52
  "@types/localtunnel": "^2.0.4",
51
53
  "@types/react": "^19.2.9",
52
54
  "@types/react-dom": "^19.2.3",
53
- "@vitejs/plugin-react": "^5.1.1",
54
55
  "autoprefixer": "^10.4.23",
55
56
  "eslint": "^9.39.1",
56
57
  "eslint-plugin-react-hooks": "^7.0.1",
package/server/index.ts CHANGED
@@ -4,6 +4,7 @@ import { join } from "path";
4
4
  import { mkdir, readdir, readFile, rm } from "fs/promises";
5
5
  import { serveStatic } from "hono/bun";
6
6
  import { streamText } from "hono/streaming";
7
+ import { tmpdir } from "os";
7
8
 
8
9
  const app = new Hono();
9
10
 
@@ -12,8 +13,8 @@ const PACKAGE_ROOT = join(import.meta.dir, "..");
12
13
  // Enable CORS for the frontend
13
14
  app.use("/*", cors());
14
15
 
15
- // Ensure tmp directory exists in the package root or system temp
16
- const TMP_DIR = join(PACKAGE_ROOT, "tmp");
16
+ // Ensure tmp directory exists in the system temp
17
+ const TMP_DIR = join(tmpdir(), "flashts-runtime");
17
18
  await mkdir(TMP_DIR, { recursive: true });
18
19
 
19
20
  // Global state management
@@ -38,19 +39,16 @@ try {
38
39
  private: true,
39
40
  dependencies: {}
40
41
  }, null, 2));
41
- console.log("-> Initialized new isolated sandbox environment");
42
42
  }
43
43
 
44
44
  // Resilient cleanup for a specific session directory
45
45
  const cleanupDir = async (dir: string) => {
46
46
  try {
47
47
  await rm(dir, { recursive: true, force: true });
48
- } catch (e) {
49
- // EBUSY is common on Windows if files are locked, ignore it
50
- }
48
+ } catch (e) {}
51
49
  };
52
50
 
53
- // Aggressive GC: attempt to delete all session directories that aren't node_modules
51
+ // Aggressive GC
54
52
  const performGC = async () => {
55
53
  try {
56
54
  const entries = await readdir(ISOLATED_ROOT, { withFileTypes: true });
@@ -62,38 +60,35 @@ const performGC = async () => {
62
60
  } catch (e) {}
63
61
  };
64
62
 
65
- // GUARANTEED Cleanup on CLI exit
63
+ const killActiveProcesses = () => {
64
+ for (const [id, proc] of vites) {
65
+ try { proc.kill(); } catch (e) {}
66
+ }
67
+ vites.clear();
68
+ if (currentExecuteProc) {
69
+ try { currentExecuteProc.kill(); } catch (e) {}
70
+ currentExecuteProc = null;
71
+ }
72
+ };
73
+
66
74
  const wipeTempSessions = async () => {
67
75
  try {
68
- // Kill all vite processes
69
- for (const [id, proc] of vites) {
70
- try { proc.kill(); } catch (e) {}
71
- }
72
- vites.clear();
73
-
74
- // Kill active execution
75
- if (currentExecuteProc) {
76
- try { currentExecuteProc.kill(); } catch (e) {}
77
- currentExecuteProc = null;
78
- }
79
-
76
+ killActiveProcesses();
80
77
  await performGC();
81
78
  } catch (e) {}
82
79
  };
83
80
 
84
- // Start with a clean slate
85
81
  await wipeTempSessions();
86
82
 
87
- process.on("SIGINT", async () => {
88
- await wipeTempSessions();
83
+ process.on("SIGINT", () => {
84
+ killActiveProcesses();
89
85
  process.exit(0);
90
86
  });
91
- process.on("SIGTERM", async () => {
92
- await wipeTempSessions();
87
+ process.on("SIGTERM", () => {
88
+ killActiveProcesses();
93
89
  process.exit(0);
94
90
  });
95
91
 
96
- // Serve static files from the client/dist directory relative to package root
97
92
  app.use("/*", serveStatic({ root: join(PACKAGE_ROOT, "client/dist") }));
98
93
 
99
94
  app.get("/api/health", (c) => c.text("FlashTS API Active"));
@@ -101,29 +96,22 @@ app.get("/api/health", (c) => c.text("FlashTS API Active"));
101
96
  app.post("/execute", async (c) => {
102
97
  const body = await c.req.json();
103
98
  const { files, entryPoint } = body;
99
+ if (!files || !entryPoint) return c.json({ error: "Invalid project state" }, 400);
104
100
 
105
- if (!files || !entryPoint) {
106
- return c.json({ error: "Invalid project state" }, 400);
107
- }
108
-
109
- // Kill any existing execution process
110
101
  if (currentExecuteProc) {
111
102
  try { currentExecuteProc.kill(); } catch (e) {}
112
103
  currentExecuteProc = null;
113
104
  }
114
105
 
115
- // Use a single static folder for all executions - efficient reuse
116
106
  const sessionDir = join(ISOLATED_ROOT, "execute-session");
117
107
  await mkdir(sessionDir, { recursive: true });
118
108
 
119
109
  for (const [name, content] of Object.entries(files)) {
120
- if (name.endsWith('/')) continue; // Skip directory markers
121
-
110
+ if (name.endsWith('/')) continue;
122
111
  const filePath = join(sessionDir, name as string);
123
112
  const parts = (name as string).split('/');
124
113
  if (parts.length > 1) {
125
- const dir = join(sessionDir, join(...parts.slice(0, -1)));
126
- await mkdir(dir, { recursive: true });
114
+ await mkdir(join(sessionDir, join(...parts.slice(0, -1))), { recursive: true });
127
115
  }
128
116
  await Bun.write(filePath, content as string);
129
117
  }
@@ -136,13 +124,9 @@ app.post("/execute", async (c) => {
136
124
  currentExecuteProc = proc;
137
125
 
138
126
  return streamText(c, async (stream) => {
139
- stream.onAbort(() => {
140
- proc.kill();
141
- });
142
-
127
+ stream.onAbort(() => { proc.kill(); });
143
128
  const stdoutReader = proc.stdout.getReader();
144
129
  const stderrReader = proc.stderr.getReader();
145
-
146
130
  const decoder = new TextDecoder();
147
131
 
148
132
  const read = async (reader: any, type: 'stdout' | 'stderr') => {
@@ -159,26 +143,16 @@ app.post("/execute", async (c) => {
159
143
  }
160
144
  };
161
145
 
162
- // Run both readers concurrently
163
- await Promise.all([
164
- read(stdoutReader, 'stdout'),
165
- read(stderrReader, 'stderr')
166
- ]);
167
-
146
+ await Promise.all([read(stdoutReader, 'stdout'), read(stderrReader, 'stderr')]);
168
147
  const exitCode = await proc.exited;
169
148
  if (currentExecuteProc === proc) currentExecuteProc = null;
170
-
171
- try {
172
- await stream.writeln(JSON.stringify({ type: 'exit', data: exitCode }));
173
- } catch (e) {}
149
+ try { await stream.writeln(JSON.stringify({ type: 'exit', data: exitCode })); } catch (e) {}
174
150
  });
175
151
  });
176
152
 
177
153
  app.post("/install", async (c) => {
178
154
  const { package: pkgName } = await c.req.json();
179
- if (!pkgName || typeof pkgName !== 'string') {
180
- return c.json({ error: "Invalid package name" }, 400);
181
- }
155
+ if (!pkgName || typeof pkgName !== 'string') return c.json({ error: "Invalid package name" }, 400);
182
156
 
183
157
  try {
184
158
  const proc = Bun.spawn([process.execPath, "add", pkgName], {
@@ -186,17 +160,12 @@ app.post("/install", async (c) => {
186
160
  stdout: "pipe",
187
161
  stderr: "pipe",
188
162
  });
189
-
190
163
  const rawOutput = await new Response(proc.stdout).text();
191
164
  const rawError = await new Response(proc.stderr).text();
192
165
  const output = rawOutput.replace(/\u001b\[[0-9;]*[a-zA-Z]/g, '');
193
166
  const error = rawError.replace(/\u001b\[[0-9;]*[a-zA-Z]/g, '');
194
167
  const exitCode = await proc.exited;
195
-
196
- if (exitCode !== 0) {
197
- return c.json({ success: false, output: error || output }, 500);
198
- }
199
-
168
+ if (exitCode !== 0) return c.json({ success: false, output: error || output }, 500);
200
169
  return c.json({ success: true, output });
201
170
  } catch (err: any) {
202
171
  return c.json({ success: false, output: err.message }, 500);
@@ -206,38 +175,25 @@ app.post("/install", async (c) => {
206
175
  app.get("/dependencies", async (c) => {
207
176
  try {
208
177
  const pkgData = JSON.parse(await readFile(ISOLATED_PKG_PATH, "utf-8"));
209
- return c.json({
210
- ...pkgData.dependencies,
211
- ...pkgData.devDependencies
212
- });
213
- } catch (e) {
214
- return c.json({});
215
- }
178
+ return c.json({ ...pkgData.dependencies, ...pkgData.devDependencies });
179
+ } catch (e) { return c.json({}); }
216
180
  });
217
181
 
218
182
  app.post("/uninstall", async (c) => {
219
183
  const { package: pkgName } = await c.req.json();
220
- if (!pkgName || typeof pkgName !== 'string') {
221
- return c.json({ error: "Invalid package name" }, 400);
222
- }
223
-
184
+ if (!pkgName || typeof pkgName !== 'string') return c.json({ error: "Invalid package name" }, 400);
224
185
  try {
225
186
  const proc = Bun.spawn([process.execPath, "remove", pkgName], {
226
187
  cwd: ISOLATED_ROOT,
227
188
  stdout: "pipe",
228
189
  stderr: "pipe",
229
190
  });
230
-
231
191
  const rawOutput = await new Response(proc.stdout).text();
232
192
  const rawError = await new Response(proc.stderr).text();
233
193
  const output = rawOutput.replace(/\u001b\[[0-9;]*[a-zA-Z]/g, '');
234
194
  const error = rawError.replace(/\u001b\[[0-9;]*[a-zA-Z]/g, '');
235
195
  const exitCode = await proc.exited;
236
-
237
- if (exitCode !== 0) {
238
- return c.json({ success: false, output: error || output }, 500);
239
- }
240
-
196
+ if (exitCode !== 0) return c.json({ success: false, output: error || output }, 500);
241
197
  return c.json({ success: true, output });
242
198
  } catch (err: any) {
243
199
  return c.json({ success: false, output: err.message }, 500);
@@ -246,89 +202,61 @@ app.post("/uninstall", async (c) => {
246
202
 
247
203
  async function getPackageTypeTree(pkgName: string) {
248
204
  let files: Record<string, string> = {};
249
-
250
205
  async function scan(dir: string, relativePath: string = "") {
251
206
  try {
252
207
  const entries = await readdir(dir, { withFileTypes: true });
253
208
  for (const entry of entries) {
254
209
  const res = join(dir, entry.name);
255
210
  const rel = relativePath ? `${relativePath}/${entry.name}` : entry.name;
256
-
257
211
  if (entry.isDirectory()) {
258
212
  if (entry.name === 'node_modules') continue;
259
213
  await scan(res, rel);
260
214
  } else if (entry.name.endsWith(".d.ts") || entry.name === "package.json") {
261
- try {
262
- files[rel] = await readFile(res, "utf-8");
263
- } catch (e) {}
215
+ try { files[rel] = await readFile(res, "utf-8"); } catch (e) {}
264
216
  }
265
217
  }
266
218
  } catch (e) {}
267
219
  }
268
-
269
- // 1. Try to find the package in ISOLATED_ROOT
270
220
  const isoPkgDir = join(ISOLATED_ROOT, "node_modules", pkgName);
271
221
  await scan(isoPkgDir);
272
-
273
- // 2. Try to find @types in ISOLATED_ROOT
274
- const typesPkgName = pkgName.startsWith('@')
275
- ? pkgName.slice(1).replace('/', '__')
276
- : pkgName;
222
+ const typesPkgName = pkgName.startsWith('@') ? pkgName.slice(1).replace('/', '__') : pkgName;
277
223
  const isoAtTypesDir = join(ISOLATED_ROOT, "node_modules", "@types", typesPkgName);
278
224
  await scan(isoAtTypesDir);
279
-
280
- // 3. Fallback to PACKAGE_ROOT (core types)
281
225
  if (Object.keys(files).length === 0) {
282
226
  const corePkgDir = join(PACKAGE_ROOT, "node_modules", pkgName);
283
227
  await scan(corePkgDir);
284
228
  const coreAtTypesDir = join(PACKAGE_ROOT, "node_modules", "@types", typesPkgName);
285
229
  await scan(coreAtTypesDir);
286
230
  }
287
-
288
231
  return Object.keys(files).length > 0 ? files : null;
289
232
  }
290
233
 
291
234
  app.get("/package-types/:pkg{.+$}", async (c) => {
292
235
  const pkgName = c.req.param("pkg");
293
236
  const tree = await getPackageTypeTree(pkgName);
294
-
295
- if (!tree) {
296
- return c.json({ error: "Types not found" }, 404);
297
- }
298
-
237
+ if (!tree) return c.json({ error: "Types not found" }, 404);
299
238
  return c.json({ tree, pkgName });
300
239
  });
301
240
 
302
241
  app.post("/preview/start", async (c) => {
303
242
  const body = await c.req.json();
304
243
  const { files, sessionId: incomingId } = body;
244
+ if (!files) return c.json({ error: "Invalid project state" }, 400);
305
245
 
306
- if (!files) {
307
- return c.json({ error: "Invalid project state" }, 400);
308
- }
309
-
310
- // Kill ALL existing vite processes to be safe
311
- for (const [id, proc] of vites) {
312
- try { proc.kill(); } catch (e) {}
313
- }
246
+ for (const [id, proc] of vites) { try { proc.kill(); } catch (e) {} }
314
247
  vites.clear();
315
248
 
316
- // GC old session folders (non-blocking)
317
249
  while (previewSessionQueue.length >= 5) {
318
250
  const oldId = previewSessionQueue.shift();
319
- if (oldId) {
320
- await cleanupDir(join(ISOLATED_ROOT, "preview-" + oldId));
321
- }
251
+ if (oldId) await cleanupDir(join(ISOLATED_ROOT, "preview-" + oldId));
322
252
  }
323
253
 
324
254
  const sessionId = incomingId || crypto.randomUUID();
325
255
  previewSessionQueue.push(sessionId);
326
-
327
256
  const sessionDir = join(ISOLATED_ROOT, "preview-" + sessionId);
328
- await cleanupDir(sessionDir); // Ensure clean slate
257
+ await cleanupDir(sessionDir);
329
258
  await mkdir(sessionDir, { recursive: true });
330
259
 
331
- // Write files
332
260
  for (const [name, content] of Object.entries(files)) {
333
261
  if (name.endsWith('/')) continue;
334
262
  const filePath = join(sessionDir, name as string);
@@ -339,7 +267,6 @@ app.post("/preview/start", async (c) => {
339
267
  await Bun.write(filePath, content as string);
340
268
  }
341
269
 
342
- // Ensure index.html exists, if not create a default one for JSX/TSX entry
343
270
  const hasIndex = Object.keys(files).some(f => f.toLowerCase() === 'index.html');
344
271
  if (!hasIndex) {
345
272
  const entryPoint = Object.keys(files).find(f => f.endsWith('.tsx') || f.endsWith('.jsx') || f.endsWith('.ts') || f.endsWith('.js')) || 'src/main.ts';
@@ -359,13 +286,12 @@ app.post("/preview/start", async (c) => {
359
286
  `.trim());
360
287
  }
361
288
 
362
- // Find an available port for Vite and HMR
363
289
  const tempServer = Bun.serve({ port: 0, fetch: () => new Response() });
364
290
  const vitePort = tempServer.port;
365
291
  tempServer.stop();
366
292
 
367
- // Create vite config to handle dependencies from root node_modules
368
293
  const normalizedPackageRoot = join(PACKAGE_ROOT).replace(/\\/g, '/');
294
+ const nodeModulesPath = join(PACKAGE_ROOT, "node_modules").replace(/\\/g, '/');
369
295
 
370
296
  const viteConfigActual = `
371
297
  import { defineConfig } from 'vite';
@@ -402,173 +328,106 @@ export default defineConfig({
402
328
  }
403
329
  }
404
330
  ],
331
+ resolve: {
332
+ alias: {
333
+ 'react': '${nodeModulesPath}/react',
334
+ 'react-dom': '${nodeModulesPath}/react-dom',
335
+ '@vitejs/plugin-react': '${nodeModulesPath}/@vitejs/plugin-react'
336
+ }
337
+ },
405
338
  root: '.',
406
339
  base: './',
407
340
  server: {
408
341
  port: ${vitePort},
409
342
  strictPort: true,
410
- host: true,
411
- hmr: {
412
- host: 'localhost',
413
- port: ${vitePort},
414
- },
415
- fs: {
416
- allow: ['${normalizedPackageRoot}']
417
- }
343
+ host: 'localhost',
344
+ hmr: { host: 'localhost', port: ${vitePort} },
345
+ fs: { allow: ['${normalizedPackageRoot}', '${TMP_DIR.replace(/\\/g, '/')}'] }
418
346
  }
419
347
  });
420
348
  `;
421
349
  await Bun.write(join(sessionDir, "vite.config.ts"), viteConfigActual);
422
350
 
423
- // Install @vitejs/plugin-react in the temp dir might be needed or we use global
424
- // For simplicity, we assume vite-related plugins are available in PACKAGE_ROOT/node_modules
425
-
426
- const proc = Bun.spawn([process.execPath, "x", "vite", "--port", String(vitePort), "--strictPort", "--host"], {
351
+ const viteBin = join(PACKAGE_ROOT, "node_modules", "vite", "bin", "vite.js");
352
+ const proc = Bun.spawn([process.execPath, viteBin, "--port", String(vitePort), "--strictPort", "--host", "localhost"], {
427
353
  cwd: sessionDir,
428
354
  stdout: "pipe",
429
355
  stderr: "pipe",
430
356
  env: {
431
357
  ...process.env,
432
- NODE_PATH: join(PACKAGE_ROOT, "node_modules")
358
+ NODE_PATH: nodeModulesPath
433
359
  }
434
360
  });
435
361
 
436
362
  vites.set(sessionId, proc);
437
363
 
438
- // Wait for port
439
364
  return new Promise<any>((resolve) => {
440
365
  let output = "";
441
- const timeout = 25000; // Increase to 25s
442
366
  const timer = setTimeout(() => {
443
- console.error("Vite preview timeout. Current output:", output);
444
- resolve(c.json({ error: "Vite timed out starting. Please try again." }, 500));
445
- }, timeout);
446
-
447
- const read = async () => {
448
- const reader = proc.stdout.getReader();
449
- const decoder = new TextDecoder();
450
-
367
+ console.error("Vite preview timeout. Current output/errors:\n", output);
368
+ resolve(c.json({ error: "Vite timed out. Check terminal for errors." }, 500));
369
+ }, 25000);
370
+
371
+ const read = async (reader: any) => {
372
+ const dr = new TextDecoder();
451
373
  while (true) {
452
374
  const { done, value } = await reader.read();
453
375
  if (done) break;
454
- const chunk = decoder.decode(value, { stream: true });
455
- const strippedChunk = chunk.replace(/\u001b\[[0-9;]*[a-zA-Z]/g, '');
456
-
457
- // Push to session-specific log listeners
458
- const listeners = previewLogs.get(sessionId);
459
- if (listeners) {
460
- listeners.forEach(l => l(strippedChunk));
461
- }
462
-
463
- output += strippedChunk;
464
-
465
- // Match various ways Vite might report the port
466
- const match = output.match(/Local:\s+http:\/\/localhost:(\d+)/i) ||
467
- output.match(/Local:\s+http:\/\/127\.0\.0\.1:(\d+)/i) ||
468
- output.match(/Local:\s+http:\/\/.*:(\d+)/i) ||
469
- output.match(/network:\s+http:\/\/.*:(\d+)/i);
470
-
376
+ const chunk = dr.decode(value, { stream: true });
377
+ const stripped = chunk.replace(/\u001b\[[0-9;]*[a-zA-Z]/g, '');
378
+ output += stripped;
379
+ const match = output.match(/Local:.*:(\d+)/i);
471
380
  if (match) {
472
381
  clearTimeout(timer);
473
- const port = match[1];
474
- const hostname = c.req.header('host')?.split(':')[0] || 'localhost';
475
- resolve(c.json({ sessionId, port, url: `http://${hostname}:${port}` }));
476
- break;
382
+ resolve(c.json({ sessionId, port: match[1], url: `http://localhost:${match[1]}` }));
477
383
  }
478
- }
479
- };
480
-
481
- const readErr = async () => {
482
- const reader = proc.stderr.getReader();
483
- const decoder = new TextDecoder();
484
- while (true) {
485
- const { done, value } = await reader.read();
486
- if (done) break;
487
- const errorText = decoder.decode(value, { stream: true });
488
- const strippedError = errorText.replace(/\u001b\[[0-9;]*[a-zA-Z]/g, '');
489
-
490
- // Push to session-specific log listeners
491
384
  const listeners = previewLogs.get(sessionId);
492
- if (listeners) {
493
- listeners.forEach(l => l(strippedError));
494
- }
495
-
496
- // Stop logging to GLOBAL server console entirely as requested
385
+ if (listeners) listeners.forEach(l => l(stripped));
497
386
  }
498
387
  };
499
-
500
- read();
501
- readErr();
388
+ read(proc.stdout.getReader());
389
+ read(proc.stderr.getReader());
502
390
  });
503
391
  });
504
392
 
505
393
  app.get("/preview/logs/:sessionId", async (c) => {
506
394
  const sessionId = c.req.param("sessionId");
507
-
508
395
  return streamText(c, async (stream) => {
509
- const listener = (log: string) => {
510
- stream.writeln(log);
511
- };
512
-
513
- if (!previewLogs.has(sessionId)) {
514
- previewLogs.set(sessionId, []);
515
- }
396
+ const listener = (log: string) => { stream.writeln(log); };
397
+ if (!previewLogs.has(sessionId)) previewLogs.set(sessionId, []);
516
398
  previewLogs.get(sessionId)!.push(listener);
517
-
518
399
  stream.onAbort(() => {
519
400
  const listeners = previewLogs.get(sessionId);
520
- if (listeners) {
521
- previewLogs.set(sessionId, listeners.filter(l => l !== listener));
522
- }
401
+ if (listeners) previewLogs.set(sessionId, listeners.filter(l => l !== listener));
523
402
  });
524
-
525
- // Keep connection alive
526
- while (true) {
527
- await new Promise(r => setTimeout(r, 1000));
528
- if (stream.closed) break;
529
- }
403
+ while (!stream.closed) { await new Promise(r => setTimeout(r, 1000)); }
530
404
  });
531
405
  });
532
406
 
533
407
  app.post("/preview/sync", async (c) => {
534
408
  const { files, sessionId } = await c.req.json();
535
409
  if (!files || !sessionId) return c.json({ error: "Missing data" }, 400);
536
-
537
410
  const sessionDir = join(ISOLATED_ROOT, "preview-" + sessionId);
538
-
539
411
  try {
540
412
  for (const [name, content] of Object.entries(files)) {
541
413
  if (name.endsWith('/')) continue;
542
414
  const filePath = join(sessionDir, name as string);
543
415
  const parts = (name as string).split('/');
544
- if (parts.length > 1) {
545
- await mkdir(join(sessionDir, join(...parts.slice(0, -1))), { recursive: true });
546
- }
416
+ if (parts.length > 1) await mkdir(join(sessionDir, join(...parts.slice(0, -1))), { recursive: true });
547
417
  await Bun.write(filePath, content as string);
548
418
  }
549
419
  return c.json({ success: true });
550
- } catch (e: any) {
551
- return c.json({ error: e.message }, 500);
552
- }
420
+ } catch (e: any) { return c.json({ error: e.message }, 500); }
553
421
  });
554
422
 
555
423
  app.get("/search", async (c) => {
556
- const query = c.req.query("q");
557
- if (!query) return c.json({ objects: [] });
558
-
424
+ const q = c.req.query("q");
425
+ if (!q) return c.json({ objects: [] });
559
426
  try {
560
- const res = await fetch(`https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(query)}&size=20`);
561
- const data = await res.json();
562
- return c.json(data);
563
- } catch (err) {
564
- return c.json({ objects: [] });
565
- }
427
+ const res = await fetch(`https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(q)}&size=20`);
428
+ return c.json(await res.json());
429
+ } catch (err) { return c.json({ objects: [] }); }
566
430
  });
567
431
 
568
432
  const port = parseInt(process.env.PORT || "3000");
569
-
570
- Bun.serve({
571
- port,
572
- fetch: app.fetch,
573
- idleTimeout: 60,
574
- });
433
+ Bun.serve({ port, fetch: app.fetch, idleTimeout: 60 });