flashts 1.0.7 → 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 +8 -0
- package/bin/cli.ts +6 -4
- package/package.json +1 -1
- package/server/index.ts +63 -208
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
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
|
+
|
|
5
13
|
## [1.0.7] - 2026-01-25
|
|
6
14
|
|
|
7
15
|
### Fixed
|
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.
|
|
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.
|
|
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
|
|
@@ -81,8 +81,10 @@ program
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
const handleExit = () => {
|
|
84
|
-
|
|
85
|
-
|
|
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
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
|
|
16
|
-
const TMP_DIR = join(
|
|
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
|
|
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;
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|
|
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,13 +286,12 @@ 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, '/');
|
|
372
295
|
|
|
373
296
|
const viteConfigActual = `
|
|
374
297
|
import { defineConfig } from 'vite';
|
|
@@ -405,174 +328,106 @@ export default defineConfig({
|
|
|
405
328
|
}
|
|
406
329
|
}
|
|
407
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
|
+
},
|
|
408
338
|
root: '.',
|
|
409
339
|
base: './',
|
|
410
340
|
server: {
|
|
411
341
|
port: ${vitePort},
|
|
412
342
|
strictPort: true,
|
|
413
|
-
host:
|
|
414
|
-
hmr: {
|
|
415
|
-
|
|
416
|
-
port: ${vitePort},
|
|
417
|
-
},
|
|
418
|
-
fs: {
|
|
419
|
-
allow: ['${normalizedPackageRoot}']
|
|
420
|
-
}
|
|
343
|
+
host: 'localhost',
|
|
344
|
+
hmr: { host: 'localhost', port: ${vitePort} },
|
|
345
|
+
fs: { allow: ['${normalizedPackageRoot}', '${TMP_DIR.replace(/\\/g, '/')}'] }
|
|
421
346
|
}
|
|
422
347
|
});
|
|
423
348
|
`;
|
|
424
349
|
await Bun.write(join(sessionDir, "vite.config.ts"), viteConfigActual);
|
|
425
350
|
|
|
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
351
|
const viteBin = join(PACKAGE_ROOT, "node_modules", "vite", "bin", "vite.js");
|
|
430
|
-
const proc = Bun.spawn([process.execPath, viteBin, "--port", String(vitePort), "--strictPort", "--host"], {
|
|
352
|
+
const proc = Bun.spawn([process.execPath, viteBin, "--port", String(vitePort), "--strictPort", "--host", "localhost"], {
|
|
431
353
|
cwd: sessionDir,
|
|
432
354
|
stdout: "pipe",
|
|
433
355
|
stderr: "pipe",
|
|
434
356
|
env: {
|
|
435
357
|
...process.env,
|
|
436
|
-
NODE_PATH:
|
|
358
|
+
NODE_PATH: nodeModulesPath
|
|
437
359
|
}
|
|
438
360
|
});
|
|
439
361
|
|
|
440
362
|
vites.set(sessionId, proc);
|
|
441
363
|
|
|
442
|
-
// Wait for port
|
|
443
364
|
return new Promise<any>((resolve) => {
|
|
444
365
|
let output = "";
|
|
445
|
-
const timeout = 25000; // Increase to 25s
|
|
446
366
|
const timer = setTimeout(() => {
|
|
447
|
-
console.error("Vite preview timeout. Current output
|
|
448
|
-
resolve(c.json({ error: "Vite timed out
|
|
449
|
-
},
|
|
450
|
-
|
|
451
|
-
const read = async () => {
|
|
452
|
-
const
|
|
453
|
-
const decoder = new TextDecoder();
|
|
454
|
-
|
|
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();
|
|
455
373
|
while (true) {
|
|
456
374
|
const { done, value } = await reader.read();
|
|
457
375
|
if (done) break;
|
|
458
|
-
const chunk =
|
|
459
|
-
const
|
|
460
|
-
|
|
461
|
-
|
|
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
|
-
|
|
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);
|
|
475
380
|
if (match) {
|
|
476
381
|
clearTimeout(timer);
|
|
477
|
-
|
|
478
|
-
const hostname = c.req.header('host')?.split(':')[0] || 'localhost';
|
|
479
|
-
resolve(c.json({ sessionId, port, url: `http://${hostname}:${port}` }));
|
|
480
|
-
break;
|
|
382
|
+
resolve(c.json({ sessionId, port: match[1], url: `http://localhost:${match[1]}` }));
|
|
481
383
|
}
|
|
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
384
|
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
|
|
385
|
+
if (listeners) listeners.forEach(l => l(stripped));
|
|
501
386
|
}
|
|
502
387
|
};
|
|
503
|
-
|
|
504
|
-
read();
|
|
505
|
-
readErr();
|
|
388
|
+
read(proc.stdout.getReader());
|
|
389
|
+
read(proc.stderr.getReader());
|
|
506
390
|
});
|
|
507
391
|
});
|
|
508
392
|
|
|
509
393
|
app.get("/preview/logs/:sessionId", async (c) => {
|
|
510
394
|
const sessionId = c.req.param("sessionId");
|
|
511
|
-
|
|
512
395
|
return streamText(c, async (stream) => {
|
|
513
|
-
const listener = (log: string) => {
|
|
514
|
-
|
|
515
|
-
};
|
|
516
|
-
|
|
517
|
-
if (!previewLogs.has(sessionId)) {
|
|
518
|
-
previewLogs.set(sessionId, []);
|
|
519
|
-
}
|
|
396
|
+
const listener = (log: string) => { stream.writeln(log); };
|
|
397
|
+
if (!previewLogs.has(sessionId)) previewLogs.set(sessionId, []);
|
|
520
398
|
previewLogs.get(sessionId)!.push(listener);
|
|
521
|
-
|
|
522
399
|
stream.onAbort(() => {
|
|
523
400
|
const listeners = previewLogs.get(sessionId);
|
|
524
|
-
if (listeners)
|
|
525
|
-
previewLogs.set(sessionId, listeners.filter(l => l !== listener));
|
|
526
|
-
}
|
|
401
|
+
if (listeners) previewLogs.set(sessionId, listeners.filter(l => l !== listener));
|
|
527
402
|
});
|
|
528
|
-
|
|
529
|
-
// Keep connection alive
|
|
530
|
-
while (true) {
|
|
531
|
-
await new Promise(r => setTimeout(r, 1000));
|
|
532
|
-
if (stream.closed) break;
|
|
533
|
-
}
|
|
403
|
+
while (!stream.closed) { await new Promise(r => setTimeout(r, 1000)); }
|
|
534
404
|
});
|
|
535
405
|
});
|
|
536
406
|
|
|
537
407
|
app.post("/preview/sync", async (c) => {
|
|
538
408
|
const { files, sessionId } = await c.req.json();
|
|
539
409
|
if (!files || !sessionId) return c.json({ error: "Missing data" }, 400);
|
|
540
|
-
|
|
541
410
|
const sessionDir = join(ISOLATED_ROOT, "preview-" + sessionId);
|
|
542
|
-
|
|
543
411
|
try {
|
|
544
412
|
for (const [name, content] of Object.entries(files)) {
|
|
545
413
|
if (name.endsWith('/')) continue;
|
|
546
414
|
const filePath = join(sessionDir, name as string);
|
|
547
415
|
const parts = (name as string).split('/');
|
|
548
|
-
if (parts.length > 1) {
|
|
549
|
-
await mkdir(join(sessionDir, join(...parts.slice(0, -1))), { recursive: true });
|
|
550
|
-
}
|
|
416
|
+
if (parts.length > 1) await mkdir(join(sessionDir, join(...parts.slice(0, -1))), { recursive: true });
|
|
551
417
|
await Bun.write(filePath, content as string);
|
|
552
418
|
}
|
|
553
419
|
return c.json({ success: true });
|
|
554
|
-
} catch (e: any) {
|
|
555
|
-
return c.json({ error: e.message }, 500);
|
|
556
|
-
}
|
|
420
|
+
} catch (e: any) { return c.json({ error: e.message }, 500); }
|
|
557
421
|
});
|
|
558
422
|
|
|
559
423
|
app.get("/search", async (c) => {
|
|
560
|
-
const
|
|
561
|
-
if (!
|
|
562
|
-
|
|
424
|
+
const q = c.req.query("q");
|
|
425
|
+
if (!q) return c.json({ objects: [] });
|
|
563
426
|
try {
|
|
564
|
-
const res = await fetch(`https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
} catch (err) {
|
|
568
|
-
return c.json({ objects: [] });
|
|
569
|
-
}
|
|
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: [] }); }
|
|
570
430
|
});
|
|
571
431
|
|
|
572
432
|
const port = parseInt(process.env.PORT || "3000");
|
|
573
|
-
|
|
574
|
-
Bun.serve({
|
|
575
|
-
port,
|
|
576
|
-
fetch: app.fetch,
|
|
577
|
-
idleTimeout: 60,
|
|
578
|
-
});
|
|
433
|
+
Bun.serve({ port, fetch: app.fetch, idleTimeout: 60 });
|