diff --git a/bun.lock b/bun.lock index 2961df1e3..2037fcfc1 100644 --- a/bun.lock +++ b/bun.lock @@ -71,6 +71,12 @@ "cheerio": "^1.2.0", "esbuild": "^0.25.12", }, + "peerDependencies": { + "hono": "^4.0.0", + }, + "optionalPeers": [ + "hono", + ], }, "packages/engine": { "name": "@hyperframes/engine", diff --git a/packages/cli/src/server/studioServer.ts b/packages/cli/src/server/studioServer.ts index 85e80097c..db5245cc2 100644 --- a/packages/cli/src/server/studioServer.ts +++ b/packages/cli/src/server/studioServer.ts @@ -1,35 +1,31 @@ /** * Embedded studio server for `hyperframes dev` outside the monorepo. * - * Serves the pre-built studio SPA and implements the project API that the - * studio expects. Ports the API logic from packages/studio/vite.config.ts. + * Uses the shared studio API module from @hyperframes/core/studio-api, + * providing a CLI-specific adapter for single-project, in-process rendering. */ import { Hono } from "hono"; import { streamSSE } from "hono/streaming"; -import { - existsSync, - readFileSync, - readdirSync, - statSync, - writeFileSync, - mkdirSync, - unlinkSync, -} from "node:fs"; -import { resolve, join, sep, basename, dirname, extname } from "node:path"; +import { existsSync, readFileSync, writeFileSync, statSync } from "node:fs"; +import { resolve, join, basename } from "node:path"; import { createProjectWatcher, type ProjectWatcher } from "./fileWatcher.js"; +import { + createStudioApi, + getMimeType, + type StudioApiAdapter, + type ResolvedProject, + type RenderJobState, +} from "@hyperframes/core/studio-api"; // ── Path resolution ───────────────────────────────────────────────────────── function resolveDistDir(): string { - // __dirname is injected by tsup banner — points to dist/ in the built CLI. - // In dev mode (tsx), it points to src/server/. const builtPath = resolve(__dirname, "studio"); if (existsSync(resolve(builtPath, "index.html"))) return builtPath; - // Fallback for dev mode: built studio is at packages/studio/dist const devPath = resolve(__dirname, "..", "..", "..", "studio", "dist"); if (existsSync(resolve(devPath, "index.html"))) return devPath; - return builtPath; // let it fail with a clear 404 + return builtPath; } function resolveRuntimePath(): string { @@ -48,134 +44,6 @@ function resolveRuntimePath(): string { return builtPath; } -// ── Safety ────────────────────────────────────────────────────────────────── - -function isSafePath(base: string, resolved: string): boolean { - const norm = resolve(base) + sep; - return resolved.startsWith(norm) || resolved === resolve(base); -} - -// ── MIME types ────────────────────────────────────────────────────────────── - -const MIME_TYPES: Record = { - ".html": "text/html", - ".js": "text/javascript", - ".css": "text/css", - ".json": "application/json", - ".svg": "image/svg+xml", - ".png": "image/png", - ".jpg": "image/jpeg", - ".jpeg": "image/jpeg", - ".webp": "image/webp", - ".gif": "image/gif", - ".mp4": "video/mp4", - ".webm": "video/webm", - ".mp3": "audio/mpeg", - ".wav": "audio/wav", - ".m4a": "audio/mp4", - ".ogg": "audio/ogg", - ".woff2": "font/woff2", - ".woff": "font/woff", - ".ttf": "font/ttf", -}; - -function getMimeType(filePath: string): string { - const ext = extname(filePath).toLowerCase(); - return MIME_TYPES[ext] ?? "application/octet-stream"; -} - -// ── File helpers ──────────────────────────────────────────────────────────── - -function walkDir(dir: string, prefix: string = ""): string[] { - const files: string[] = []; - for (const entry of readdirSync(dir, { withFileTypes: true })) { - const rel = prefix ? `${prefix}/${entry.name}` : entry.name; - if (entry.isDirectory()) { - files.push(...walkDir(join(dir, entry.name), rel)); - } else { - files.push(rel); - } - } - return files; -} - -function serveStaticFile(filePath: string): Response | null { - if (!existsSync(filePath) || !statSync(filePath).isFile()) return null; - const mime = getMimeType(filePath); - const content = readFileSync(filePath); - return new Response(content, { - headers: { "Content-Type": mime, "Cache-Control": "no-store" }, - }); -} - -// ── Sub-composition builder ───────────────────────────────────────────────── -// Ports vite.config.ts lines 216-301 - -function buildSubCompositionHtml( - projectDir: string, - compPath: string, - runtimeUrl: string, -): string | null { - const compFile = resolve(projectDir, compPath); - if (!isSafePath(projectDir, compFile) || !existsSync(compFile) || !statSync(compFile).isFile()) { - return null; - } - - let rawComp = readFileSync(compFile, "utf-8"); - - // Extract content from