flashts 1.0.7 → 1.0.9

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