mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 23:00:03 +00:00
refactor(core): extract shared studio API module (#113)
## Summary Extracts all studio API routes into a shared Hono-based module at `@hyperframes/core/studio-api`. ### 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` ### What this PR does - Creates the shared module with all API routes extracted from both `vite.config.ts` and `studioServer.ts` - Both consumers will be refactored in follow-up commits to mount this module with their own adapter ### What stays in each consumer - **Vite**: SSR module loading, Puppeteer thumbnails, file watcher + HMR, producer HTTP proxy, multi-project scanning - **CLI**: in-process `executeRenderJob`, local runtime serving, browser management, SPA static file serving ### Follow-up needed - [ ] Refactor `packages/studio/vite.config.ts` to use `createStudioApi(adapter)` via `@hono/node-server`'s `getRequestListener` - [ ] Refactor `packages/cli/src/server/studioServer.ts` to use `createStudioApi(adapter)` - [ ] Add `./studio-api` export path to `packages/core/package.json` - [ ] Add `hono` as peer dependency of `@hyperframes/core` ## Test plan - [ ] Verify shared module compiles without type errors - [ ] After consumer refactoring: all studio features work identically via both vite dev and CLI embedded servers 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
export const MIME_TYPES: Record<string, string> = {
|
||||
".html": "text/html",
|
||||
".css": "text/css",
|
||||
".js": "text/javascript",
|
||||
".mjs": "text/javascript",
|
||||
".json": "application/json",
|
||||
".svg": "image/svg+xml",
|
||||
".png": "image/png",
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".gif": "image/gif",
|
||||
".webp": "image/webp",
|
||||
".ico": "image/x-icon",
|
||||
".mp4": "video/mp4",
|
||||
".webm": "video/webm",
|
||||
".mp3": "audio/mpeg",
|
||||
".wav": "audio/wav",
|
||||
".ogg": "audio/ogg",
|
||||
".m4a": "audio/mp4",
|
||||
".woff": "font/woff",
|
||||
".woff2": "font/woff2",
|
||||
".ttf": "font/ttf",
|
||||
".otf": "font/otf",
|
||||
".txt": "text/plain",
|
||||
".md": "text/markdown",
|
||||
};
|
||||
|
||||
export function getMimeType(path: string): string {
|
||||
const ext = path.slice(path.lastIndexOf(".")).toLowerCase();
|
||||
return MIME_TYPES[ext] || "application/octet-stream";
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { resolve, sep, join } from "node:path";
|
||||
import { readdirSync } from "node:fs";
|
||||
|
||||
/** Reject paths that escape the project directory. */
|
||||
export function isSafePath(base: string, resolved: string): boolean {
|
||||
const norm = resolve(base) + sep;
|
||||
return resolved.startsWith(norm) || resolved === resolve(base);
|
||||
}
|
||||
|
||||
const IGNORE_DIRS = new Set([".thumbnails", "node_modules", ".git"]);
|
||||
|
||||
/** Recursively walk a directory and return relative file paths. */
|
||||
export function walkDir(dir: string, prefix = ""): string[] {
|
||||
const files: string[] = [];
|
||||
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||||
if (IGNORE_DIRS.has(entry.name)) continue;
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
/**
|
||||
* Build a standalone HTML page for a sub-composition.
|
||||
*
|
||||
* Uses the project's own index.html `<head>` so all dependencies (GSAP, fonts,
|
||||
* Lottie, reset styles, runtime) are preserved — instead of building a minimal
|
||||
* page from scratch that would miss important scripts/styles.
|
||||
*/
|
||||
export function buildSubCompositionHtml(
|
||||
projectDir: string,
|
||||
compPath: string,
|
||||
runtimeUrl: string,
|
||||
baseHref?: string,
|
||||
): string | null {
|
||||
const compFile = join(projectDir, compPath);
|
||||
if (!existsSync(compFile)) return null;
|
||||
|
||||
const rawComp = readFileSync(compFile, "utf-8");
|
||||
|
||||
// Extract content from <template> wrapper (compositions are always templates)
|
||||
const templateMatch = rawComp.match(/<template[^>]*>([\s\S]*)<\/template>/i);
|
||||
const content = templateMatch?.[1] ?? rawComp;
|
||||
|
||||
// Use the project's index.html <head> to preserve all dependencies
|
||||
const indexPath = join(projectDir, "index.html");
|
||||
let headContent = "";
|
||||
|
||||
if (existsSync(indexPath)) {
|
||||
const indexHtml = readFileSync(indexPath, "utf-8");
|
||||
const headMatch = indexHtml.match(/<head[^>]*>([\s\S]*?)<\/head>/i);
|
||||
headContent = headMatch?.[1] ?? "";
|
||||
}
|
||||
|
||||
// Inject <base> for relative asset resolution (before other tags)
|
||||
if (baseHref && !headContent.includes("<base")) {
|
||||
headContent = `<base href="${baseHref}">\n${headContent}`;
|
||||
}
|
||||
|
||||
// Ensure runtime is present (might differ from the one in index.html)
|
||||
if (
|
||||
!headContent.includes("hyperframe.runtime") &&
|
||||
!headContent.includes("hyperframes-preview-runtime")
|
||||
) {
|
||||
headContent += `\n<script data-hyperframes-preview-runtime="1" src="${runtimeUrl}"></script>`;
|
||||
}
|
||||
|
||||
// Fallback: if no index.html head was found, add minimal deps
|
||||
if (!headContent.includes("gsap")) {
|
||||
headContent += `\n<script src="https://cdn.jsdelivr.net/npm/gsap@3/dist/gsap.min.js"></script>`;
|
||||
}
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
${headContent}
|
||||
</head>
|
||||
<body>
|
||||
<script>window.__timelines=window.__timelines||{};</script>
|
||||
${content}
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
Reference in New Issue
Block a user