website-builde / server.ts
Daviddolor's picture
Single-process Chromium, fixed visual-polish prompt, Docker config for HF Space
ec675f2
Raw
History Blame Contribute Delete
4.4 kB
import "dotenv/config";
import express from "express";
import { chromium, Browser } from "playwright";
import { crawlPage } from "./lib/crawler";
import { chunkData } from "./lib/chunker";
import { formatAnalysisForPrompt, buildWebsitePrompt } from "./lib/ai-prompt";
import { generateWebsite } from "./lib/llm";
import { crawlQueue } from "./lib/queue";
import { parseGeneratedFiles } from "./lib/file-parser";
import { writeProjectFiles } from "./lib/project-writer";
import { validateAndSave } from "./lib/validator";
const API_KEY = process.env.CRAWL_API_KEY || "d3v-crawl-server-key-2026";
const PORT = Number(process.env.PORT) || 7860;
const HOST = "0.0.0.0";
let browser: Browser | undefined;
async function launchBrowser(): Promise<Browser> {
console.log("Launching Chromium in-process...");
return chromium.launch({
headless: true,
args: ["--no-sandbox", "--disable-dev-shm-usage", "--disable-gpu"],
});
}
async function ensureBrowser(): Promise<Browser> {
if (!browser || !browser.isConnected()) {
browser = await launchBrowser();
}
return browser;
}
function withTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise<T> {
return Promise.race([
promise,
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms)
),
]);
}
const app = express();
app.use(express.json());
app.use((req, res, next) => {
if (req.path === "/health") return next();
const auth = req.headers.authorization;
if (auth !== `Bearer ${API_KEY}`) return res.status(401).json({ error: "unauthorized" });
next();
});
app.get("/crawl", async (req, res) => {
const url = req.query.url as string;
if (!url) return res.status(400).json({ error: "url query param required" });
try {
const result = await crawlQueue.run(async () => {
const b = await ensureBrowser();
return crawlPage(b, url);
});
const out = req.query.chunked === "true" ? chunkData(result) : result;
res.json(out);
} catch (err: any) {
res.status(500).json({ error: err.message });
}
});
app.post("/build", async (req, res) => {
const { prompt, reference, validate } = req.body || {};
if (!prompt) return res.status(400).json({ error: "prompt is required" });
try {
let analysisText: string | undefined;
if (reference) {
console.log("[build] starting crawl of", reference);
try {
const crawled = await withTimeout(
crawlQueue.run(async () => {
const b = await ensureBrowser();
return crawlPage(b, reference);
}),
70000,
"crawl"
);
console.log("[build] crawl finished");
analysisText = formatAnalysisForPrompt(crawled);
} catch (err: any) {
console.log("[build] crawl failed/timed out, continuing without reference:", err.message);
}
}
const fullPrompt = buildWebsitePrompt(prompt, analysisText);
console.log("[build] calling LLM...");
const rawResult = await generateWebsite(fullPrompt);
console.log("[build] LLM call finished, length:", rawResult.length);
const files = parseGeneratedFiles(rawResult);
if (files.length === 0) {
res.json({ prompt, reference: reference || null, result: rawResult, files: [] });
return;
}
const slug = prompt.toLowerCase().replace(/[^a-z0-9]+/g, "-");
const { projectDir, written } = writeProjectFiles(files, slug);
let validation = null;
if (validate) {
console.log("[build] validating (npm install + npm run build)...");
validation = await validateAndSave(projectDir);
console.log("[build] validation finished, success:", validation.success);
}
res.json({ prompt, reference: reference || null, projectDir, files: written, validation });
} catch (err: any) {
res.status(500).json({ error: err.message });
}
});
app.get("/health", (_, res) => {
res.json({ status: "ok", browserConnected: browser ? browser.isConnected() : false });
});
async function start() {
await ensureBrowser();
app.listen(PORT, HOST, () => console.log(`API server alive on http://${HOST}:${PORT}`));
}
async function shutdown() {
console.log("Shutting down API server and closing browser...");
if (browser) await browser.close();
process.exit(0);
}
process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);
start();