Spaces:
Sleeping
Sleeping
| 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(); | |