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 +15 -0
- package/bin/cli.ts +12 -9
- package/package.json +4 -3
- package/server/index.ts +80 -221
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.
|
|
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
|
|
@@ -80,13 +80,16 @@ program
|
|
|
80
80
|
}, 1000);
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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.
|
|
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
|
|
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 });
|
|
@@ -62,38 +60,35 @@ const performGC = async () => {
|
|
|
62
60
|
} catch (e) {}
|
|
63
61
|
};
|
|
64
62
|
|
|
65
|
-
|
|
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
|
-
|
|
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",
|
|
88
|
-
|
|
83
|
+
process.on("SIGINT", () => {
|
|
84
|
+
killActiveProcesses();
|
|
89
85
|
process.exit(0);
|
|
90
86
|
});
|
|
91
|
-
process.on("SIGTERM",
|
|
92
|
-
|
|
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;
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|
|
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:
|
|
411
|
-
hmr: {
|
|
412
|
-
|
|
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
|
-
|
|
424
|
-
|
|
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:
|
|
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
|
|
444
|
-
resolve(c.json({ error: "Vite timed out
|
|
445
|
-
},
|
|
446
|
-
|
|
447
|
-
const read = async () => {
|
|
448
|
-
const
|
|
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 =
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
557
|
-
if (!
|
|
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(
|
|
561
|
-
|
|
562
|
-
|
|
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 });
|