From 644ad1251107b814b28339c4beb99ed6d83dba1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Sat, 28 Mar 2026 02:36:48 -0400 Subject: [PATCH] refactor(core): extract shared studio API module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Create @hyperframes/core/studio-api — a Hono-based shared API module that both the vite dev server and CLI embedded server can mount. Architecture: - StudioApiAdapter interface: consumers inject host-specific behavior (project resolution, bundling, rendering, thumbnails) - Shared route modules: projects, files, preview, lint, render, thumbnail - Shared helpers: isSafePath, walkDir, getMimeType, buildSubCompositionHtml This module contains all API route logic in one place. Both consumers will be refactored to mount this module with their own adapter: - Vite: SSR module loading, Puppeteer thumbnails, producer HTTP proxy - CLI: in-process rendering, local runtime, single project Co-Authored-By: Claude Sonnet 4.6 (1M context) --- bun.lock | 6 + packages/cli/src/server/studioServer.ts | 597 ++++-------------- packages/core/package.json | 20 +- .../core/src/studio-api/createStudioApi.ts | 27 + packages/core/src/studio-api/helpers/mime.ts | 31 + .../core/src/studio-api/helpers/safePath.ts | 25 + .../src/studio-api/helpers/subComposition.ts | 64 ++ packages/core/src/studio-api/index.ts | 5 + packages/core/src/studio-api/routes/files.ts | 36 ++ packages/core/src/studio-api/routes/lint.ts | 34 + .../core/src/studio-api/routes/preview.ts | 87 +++ .../core/src/studio-api/routes/projects.ts | 30 + packages/core/src/studio-api/routes/render.ts | 200 ++++++ .../core/src/studio-api/routes/thumbnail.ts | 75 +++ packages/core/src/studio-api/types.ts | 78 +++ 15 files changed, 846 insertions(+), 469 deletions(-) create mode 100644 packages/core/src/studio-api/createStudioApi.ts create mode 100644 packages/core/src/studio-api/helpers/mime.ts create mode 100644 packages/core/src/studio-api/helpers/safePath.ts create mode 100644 packages/core/src/studio-api/helpers/subComposition.ts create mode 100644 packages/core/src/studio-api/index.ts create mode 100644 packages/core/src/studio-api/routes/files.ts create mode 100644 packages/core/src/studio-api/routes/lint.ts create mode 100644 packages/core/src/studio-api/routes/preview.ts create mode 100644 packages/core/src/studio-api/routes/projects.ts create mode 100644 packages/core/src/studio-api/routes/render.ts create mode 100644 packages/core/src/studio-api/routes/thumbnail.ts create mode 100644 packages/core/src/studio-api/types.ts 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