(options: StudioServerOptions)
| 276 | } |
| 277 | |
| 278 | export function createStudioServer(options: StudioServerOptions): StudioServer { |
| 279 | const { projectDir, projectName } = options; |
| 280 | const projectId = projectName || basename(projectDir); |
| 281 | const studioDir = resolveDistDir(); |
| 282 | const runtimePath = resolveRuntimePath(); |
| 283 | const watcher = createProjectWatcher(projectDir); |
| 284 | |
| 285 | // ── CLI adapter for the shared studio API ────────────────────────────── |
| 286 | |
| 287 | const project: ResolvedProject = { id: projectId, dir: projectDir, title: projectId }; |
| 288 | let cachedProjectSignature: string | null = null; |
| 289 | watcher.addListener(() => { |
| 290 | cachedProjectSignature = null; |
| 291 | }); |
| 292 | |
| 293 | const adapter: StudioApiAdapter = { |
| 294 | listProjects: () => [project], |
| 295 | |
| 296 | resolveProject: (id: string) => (id === projectId ? project : null), |
| 297 | |
| 298 | async bundle(dir: string): Promise<string | null> { |
| 299 | try { |
| 300 | const { bundleToSingleHtml } = await import("@hyperframes/core/compiler"); |
| 301 | // Studio dev server: ask the bundler for an empty `src=""` placeholder so |
| 302 | // we can point it at our hot-reloadable local runtime endpoint. Inlining |
| 303 | // ~150 KB of runtime body on every preview render would defeat browser |
| 304 | // caching across composition edits. |
| 305 | let html = await bundleToSingleHtml(dir, { runtime: "placeholder" }); |
| 306 | html = html.replace( |
| 307 | 'data-hyperframes-preview-runtime="1" src=""', |
| 308 | 'data-hyperframes-preview-runtime="1" src="/api/runtime.js"', |
| 309 | ); |
| 310 | return html; |
| 311 | } catch (err) { |
| 312 | console.error("[studio] Bundle failed:", err); |
| 313 | return null; |
| 314 | } |
| 315 | }, |
| 316 | |
| 317 | async transformPreviewHtml({ html, project }) { |
| 318 | const { injectDeterministicFontFaces } = |
| 319 | await import("../../../producer/src/services/deterministicFonts.js"); |
| 320 | const { prepareAnimatedGifInputs } = |
| 321 | await import("../../../producer/src/services/animatedGifPrep.js"); |
| 322 | const { downloadToTemp } = await import("../../../producer/src/utils/urlDownloader.js"); |
| 323 | const gifOutputDir = join(project.dir, ".hyperframes", "prepared-assets", "gif"); |
| 324 | const gifDownloadDir = join(project.dir, ".hyperframes", "prepared-assets", "downloads"); |
| 325 | const prepared = await prepareAnimatedGifInputs(html, { |
| 326 | projectDir: project.dir, |
| 327 | downloadDir: gifDownloadDir, |
| 328 | outputDir: gifOutputDir, |
| 329 | outputSrcPrefix: ".hyperframes/prepared-assets/gif", |
| 330 | cacheDir: gifOutputDir, |
| 331 | sourceAssets: await downloadRemoteGifImageSources(html, gifDownloadDir, downloadToTemp), |
| 332 | }); |
| 333 | return injectDeterministicFontFaces(prepared.html); |
| 334 | }, |
| 335 |
no test coverage detected