Skip to content

Commit 19b8969

Browse files
committed
fix railway npm issues
1 parent ff842be commit 19b8969

File tree

3 files changed

+69
-5
lines changed

3 files changed

+69
-5
lines changed

scripts/prebuild-template.mjs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,11 @@ for (const [filePath, content] of Object.entries(TEMPLATES)) {
4848
}
4949

5050
console.log("[prebuild] Running npm install...");
51-
execSync("npm install", { cwd: BASE_DIR, stdio: "inherit", timeout: 120_000 });
51+
execSync("npm install --include=dev", {
52+
cwd: BASE_DIR,
53+
stdio: "inherit",
54+
timeout: 120_000,
55+
});
5256

5357
console.log("[prebuild] Installing shadcn components...");
5458
try {

src/app/api/widget/[id]/[[...path]]/route.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,29 @@ const LOADING_HTML = `<!DOCTYPE html>
1414
</body>
1515
</html>`;
1616

17+
function escapeHtml(value: string): string {
18+
return value
19+
.replaceAll("&", "&amp;")
20+
.replaceAll("<", "&lt;")
21+
.replaceAll(">", "&gt;")
22+
.replaceAll('"', "&quot;")
23+
.replaceAll("'", "&#39;");
24+
}
25+
26+
function errorHtml(message?: string): string {
27+
const detail = message ? `<pre style="margin:0;white-space:pre-wrap;word-break:break-word;color:#f4f4f5;max-width:640px">${escapeHtml(message)}</pre>` : "<div>No error details were captured.</div>";
28+
return `<!DOCTYPE html>
29+
<html class="dark">
30+
<head><meta charset="UTF-8"><meta http-equiv="refresh" content="30"></head>
31+
<body style="margin:0;background:#27272a;display:flex;align-items:center;justify-content:center;height:100vh;font-family:ui-monospace,monospace;color:#a1a1aa;font-size:12px;padding:16px;">
32+
<div style="max-width:680px;width:100%;border:1px solid #3f3f46;background:#18181b;padding:16px">
33+
<div style="color:#f59e0b;text-transform:uppercase;letter-spacing:0.08em;font-size:11px;margin-bottom:8px">Widget build failed</div>
34+
${detail}
35+
</div>
36+
</body>
37+
</html>`;
38+
}
39+
1740
export async function GET(
1841
req: NextRequest,
1942
{ params }: { params: Promise<{ id: string; path?: string[] }> }
@@ -30,6 +53,13 @@ export async function GET(
3053

3154
const widget = await ensureWidget(id);
3255

56+
if (widget.status === "error") {
57+
return new Response(errorHtml(widget.error), {
58+
status: 200,
59+
headers: { "Content-Type": "text/html; charset=utf-8" },
60+
});
61+
}
62+
3363
if (widget.status !== "ready") {
3464
return new Response(LOADING_HTML, {
3565
status: 200,

src/lib/widget-runner.ts

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,15 @@ import {
2828
} from "@/db/widgets";
2929

3030
const execAsync = promisify(execCb);
31+
const TEMPLATE_INSTALL_CMD = "npm install --include=dev";
3132

3233
// ── Types ──
3334

3435
interface WidgetStatus {
3536
status: "building" | "ready" | "error";
3637
port: number;
3738
startedAt?: number;
39+
error?: string;
3840
}
3941

4042
interface WidgetSandbox {
@@ -111,6 +113,18 @@ const PREBAKED_DIR = join(process.cwd(), ".cache", "widget-base-template");
111113
let baseTemplateDir: string | null = null;
112114
let baseTemplatePromise: Promise<string> | null = null;
113115

116+
function getBuildErrorMessage(error: unknown): string {
117+
if (error && typeof error === "object") {
118+
const err = error as { stderr?: string | Buffer; stdout?: string | Buffer; message?: string };
119+
const detail = err.stderr ?? err.stdout ?? err.message;
120+
if (detail) {
121+
return String(detail).trim().split("\n").slice(-8).join("\n");
122+
}
123+
}
124+
if (typeof error === "string") return error;
125+
return "Unknown widget build error";
126+
}
127+
114128
async function ensureBaseTemplate(): Promise<string> {
115129
if (baseTemplateDir && existsSync(join(baseTemplateDir, "node_modules"))) {
116130
return baseTemplateDir;
@@ -137,7 +151,7 @@ async function ensureBaseTemplate(): Promise<string> {
137151
writeFileSync(full, content);
138152
}
139153

140-
await execAsync("npm install", { cwd: dir, timeout: 120_000 });
154+
await execAsync(TEMPLATE_INSTALL_CMD, { cwd: dir, timeout: 120_000 });
141155
console.log("[secure-exec] npm install done");
142156

143157
try {
@@ -328,7 +342,12 @@ async function doBuild(widgetId: string): Promise<void> {
328342
try {
329343
const files = getWidgetFiles(widgetId);
330344
if (!files["src/App.tsx"]) {
331-
widgetStatuses.set(widgetId, { status: "error", port });
345+
widgetStatuses.set(widgetId, {
346+
status: "error",
347+
port,
348+
startedAt: Date.now(),
349+
error: "Missing src/App.tsx",
350+
});
332351
console.error(`[secure-exec] No src/App.tsx for ${widgetId}`);
333352
return;
334353
}
@@ -377,7 +396,12 @@ async function doBuild(widgetId: string): Promise<void> {
377396
console.log(`[secure-exec] Widget ${widgetId} serving on port ${port}`);
378397
} catch (err) {
379398
console.error(`[secure-exec] Build error for ${widgetId}:`, err);
380-
widgetStatuses.set(widgetId, { status: "error", port });
399+
widgetStatuses.set(widgetId, {
400+
status: "error",
401+
port,
402+
startedAt: Date.now(),
403+
error: getBuildErrorMessage(err),
404+
});
381405
}
382406
}
383407

@@ -392,12 +416,18 @@ export async function buildWidget(widgetId: string): Promise<void> {
392416
}
393417

394418
const BUILD_TIMEOUT_MS = 120_000;
419+
const ERROR_RETRY_MS = 30_000;
395420

396421
export async function ensureWidget(widgetId: string): Promise<WidgetStatus> {
397422
const existing = widgetStatuses.get(widgetId);
398423
if (existing?.status === "ready" && widgetSandboxes.has(widgetId)) return existing;
399424
const isStale = existing?.status === "building" && existing.startedAt && Date.now() - existing.startedAt > BUILD_TIMEOUT_MS;
425+
const shouldRetryError =
426+
existing?.status === "error" &&
427+
existing.startedAt &&
428+
Date.now() - existing.startedAt > ERROR_RETRY_MS;
400429
if (existing?.status === "building" && !isStale) return existing;
430+
if (existing?.status === "error" && !shouldRetryError) return existing;
401431

402432
const port = await getPort({ port: portNumbers(4100, 4999) });
403433
const status: WidgetStatus = { status: "building", port, startedAt: Date.now() };
@@ -408,7 +438,7 @@ export async function ensureWidget(widgetId: string): Promise<WidgetStatus> {
408438

409439
export async function rebuildWidget(widgetId: string): Promise<WidgetStatus> {
410440
const port = await getPort({ port: portNumbers(4100, 4999) });
411-
const status: WidgetStatus = { status: "building", port };
441+
const status: WidgetStatus = { status: "building", port, startedAt: Date.now() };
412442
widgetStatuses.set(widgetId, status);
413443
buildWidget(widgetId).catch((err) => console.error(`[secure-exec] Rebuild failed for ${widgetId}:`, err));
414444
return status;

0 commit comments

Comments
 (0)