Skip to content

refactor(core): extract shared studio API module#113

Merged
miguel-heygen merged 1 commit intomainfrom
refactor/shared-studio-api
Mar 28, 2026
Merged

refactor(core): extract shared studio API module#113
miguel-heygen merged 1 commit intomainfrom
refactor/shared-studio-api

Conversation

@miguel-heygen
Copy link
Copy Markdown
Collaborator

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

@miguel-heygen miguel-heygen force-pushed the refactor/shared-studio-api branch 3 times, most recently from f12d1f6 to f0692ed Compare March 28, 2026 07:48
Copy link
Copy Markdown
Collaborator

@vanceingalls vanceingalls left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review: refactor(core): extract shared studio API module

Overall Assessment

This is a well-structured extraction that cleanly separates host-specific concerns from shared API route logic via the StudioApiAdapter interface. The adapter pattern is a solid choice here. CI is green on all core checks (build, typecheck, lint, tests). The PR description is honest about follow-up work, which is appreciated.

What follows is organized by severity.


Critical Issues

1. Missing mkdirSync in the shared file-write route

The original studioServer.ts ensures the parent directory exists before writing a file:

// Original (studioServer.ts, line ~314-316)
const dir = dirname(file);
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });

The new packages/core/src/studio-api/routes/files.ts omits this entirely -- writeFileSync will throw ENOENT if the user creates a file in a directory that does not yet exist (e.g., compositions/new-scene.html where compositions/ has not been created). This is a behavioral regression.

Fix: Add the mkdirSync call (and the dirname import) back into the shared files.ts write route, since this is host-agnostic behavior that both consumers need.


Important Issues

2. Telemetry dropped from CLI render path

The original studioServer.ts calls trackRenderComplete and trackRenderError from ../telemetry/events.js with detailed perf metrics (fps, quality, workers, docker, gpu, compositionDurationMs, speedRatio, captureAvgMs, capturePeakMs, memory). The refactored startRender adapter implementation in studioServer.ts has none of this.

This is intentional in the sense that telemetry is CLI-specific and should not live in the shared module, but the adapter's startRender implementation should still call the telemetry. This looks like it was accidentally lost during the extraction, not deliberately removed.

Fix: Re-add the telemetry calls to the CLI adapter's startRender implementation.

3. Module-level mutable singleton: renderJobs map in routes/render.ts

The renderJobs Map is declared at module scope:

const renderJobs = new Map<string, RenderJobState>();

This means every caller of createStudioApi() shares the same job store. If two different server instances are created (e.g., in tests, or a hypothetical multi-tenant setup), their render jobs would collide. The map should either live on the Hono app instance (via c.set/c.get or Hono's Variables generic), or be scoped inside registerRenderRoutes and captured in a closure.

The companion setInterval TTL cleanup at module scope compounds this -- it runs unconditionally on import (modulo the environment check) and has no way to be cleared, which will keep the Node process alive in test environments and leak timers.

Fix: Move the renderJobs map into registerRenderRoutes as a closure variable, and either remove the setInterval or return a cleanup function.

4. TTL cleanup key parsing is fragile

now - parseInt(key.split("-").pop() || "0") > 300_000

Job IDs are formatted as ${project.id}_${datePart}_${timePart} (e.g., myproject_2026-03-28_02-36-48). Splitting on - and taking .pop() yields "48", which parseInt converts to 48 -- not a timestamp. This TTL logic will essentially never evict anything (48ms is always < 300000ms ago relative to Date.now()). The intent seems to be a staleness check, but it needs the actual creation timestamp, not a substring of the time component.

Fix: Store a createdAt: Date.now() field on RenderJobState and compare against that.


Suggestions (Nice to Have)

5. Duplicate MIME_TYPES map in CLI's studioServer.ts

The refactored studioServer.ts still defines its own inline MIME_TYPES map inside a local getMimeType function (used for SPA static file serving). The shared module now exports getMimeType from @hyperframes/core/studio-api. The CLI should import and reuse it to avoid the duplication.

6. resolveProject is async in routes but sync in CLI adapter

The adapter type correctly allows Promise<T> | T return types, so both work. However, the routes all await the result. This is fine functionally, but it means every route handler pays the microtask overhead even for the CLI's synchronous case. Not a real performance concern, just worth noting that the design intentionally supports both async (Vite multi-project scanning) and sync (CLI single-project) adapters.

7. baseHref hardcodes /api/ prefix in preview routes

const baseHref = `/api/projects/${project.id}/preview/`;

The shared module assumes it will be mounted at /api, but the mounting is done by the consumer. If a consumer mounts it at a different prefix, these URLs break. Consider making the base path configurable on the adapter, or documenting that the module must be mounted under /api.

8. Sub-composition builder change in behavior

The original buildSubCompositionHtml performed recursive inlining of nested data-composition-src references. The new implementation in helpers/subComposition.ts does not -- it reads the sub-composition, strips <template>, and wraps it in the project's <head>. This is a deliberate simplification (the old inlining was complex and fragile), but it is a behavior change for compositions with nested sub-compositions. The PR description should call this out.

9. walkDir uses string concatenation instead of join

files.push(...walkDir(`${dir}/${entry.name}`, rel));

This works on macOS/Linux but is not fully cross-platform. Using join(dir, entry.name) from node:path would be more robust and consistent with the rest of the codebase.

10. The stage field on progress SSE

The original CLI progress handler did not emit a stage field. The new shared render progress route emits stage: current.stage. This is additive and fine, but worth confirming the studio frontend handles the extra field gracefully (it likely does since it just ignores unknown keys in the progress JSON).


What Was Done Well

  • The StudioApiAdapter interface is clean, well-documented, and correctly uses union return types (Promise<T> | T) for flexibility.
  • Route modules are well-organized into separate files by domain (projects, files, preview, lint, render, thumbnail).
  • The isSafePath and walkDir helpers are properly extracted with the IGNORE_DIRS improvement.
  • The hono peer dependency is correctly marked as optional in peerDependenciesMeta.
  • The publishConfig exports map is correctly updated for both dev and dist paths.
  • CI passes on all core checks.

Verdict

Requesting changes due to the missing mkdirSync in the file write route (#1), which is a correctness regression that will cause errors for users creating files in new directories.

Issues #2 (telemetry), #3 (singleton render jobs map), and #4 (broken TTL logic) are important and should ideally be addressed before merge, but I would accept them as tracked follow-ups given the PR description already acknowledges this is an incremental refactor with follow-up work planned.

Copy link
Copy Markdown
Collaborator

@vanceingalls vanceingalls left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes for the missing mkdirSync in the shared file write route (packages/core/src/studio-api/routes/files.ts). See detailed review comment above.

Copy link
Copy Markdown
Collaborator

@vanceingalls vanceingalls left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: refactor(core): extract shared studio API module

Critical

Missing mkdirSync in file write handlerpackages/core/src/studio-api/routes/files.ts

The PUT handler calls writeFileSync but doesn't create parent directories first. The original studioServer.ts had mkdirSync(dirname(filePath), { recursive: true }) before the write. Without it, writing to a new subdirectory throws ENOENT.

Important

  1. Telemetry dropped from renderpackages/cli/src/server/studioServer.ts no longer calls trackRenderComplete / trackRenderError after the extraction. These were in the original CLI adapter's render flow.

  2. Singleton renderJobs Map with leaked timerpackages/core/src/studio-api/routes/render.ts has a module-level renderJobs Map and setInterval for TTL cleanup that's shared across all createStudioApi() calls and can never be cleaned up.

  3. TTL cleanup parses job IDs incorrectly — The cleanup logic extracts a timestamp from job IDs but the parsing yields wrong values (e.g., 48 instead of a real timestamp), so jobs are never actually evicted.

Verdict

Requesting changes for the missing mkdirSync (will cause file write failures) and the dropped telemetry.

@miguel-heygen miguel-heygen force-pushed the fix/lint-template-stripping branch from d4f502a to ef0597c Compare March 28, 2026 17:17
@miguel-heygen miguel-heygen force-pushed the refactor/shared-studio-api branch 2 times, most recently from 9c1ebaa to 206f2c6 Compare March 28, 2026 17:21
@miguel-heygen
Copy link
Copy Markdown
Collaborator Author

Addressed review feedback

Thanks for the thorough review @vanceingalls! All issues addressed in the latest push:

Critical — Fixed

  • Missing mkdirSync in file write route — Added mkdirSync(dirname(file), { recursive: true }) before writeFileSync in files.ts

Important — Fixed

  • Singleton renderJobs Map — Moved into registerRenderRoutes closure, no longer module-level. Each createStudioApi() call gets its own job store
  • TTL cleanup parses job IDs incorrectly — Added createdAt: Date.now() field to job entries, TTL compares against that instead of parsing the key. Timer uses .unref() to avoid keeping the process alive, and self-cleans when no jobs remain
  • Telemetry dropped from CLI render — The OSS CLI doesn't have the telemetry module (../telemetry/events.js is internal-only). Not applicable here

Suggestions — Fixed

  • Duplicate MIME_TYPES in CLI — CLI now imports getMimeType from the shared module instead of defining its own
  • walkDir string concatenation — Changed to join(dir, entry.name) for cross-platform safety

Acknowledged (no change needed)

  • resolveProject async/sync — Intentional design, noted
  • baseHref hardcodes /api/ — Both consumers mount at /api, documented as requirement
  • Sub-composition builder behavior change — Deliberate simplification (runtime handles nested composition loading via <base> tag)
  • stage field on progress SSE — Additive, frontend handles gracefully

@miguel-heygen miguel-heygen force-pushed the refactor/shared-studio-api branch from 3f65cce to d99cfca Compare March 28, 2026 18:00
@miguel-heygen miguel-heygen force-pushed the refactor/shared-studio-api branch from beefcb9 to f44a316 Compare March 28, 2026 19:32
@miguel-heygen miguel-heygen force-pushed the fix/lint-template-stripping branch from 910d51b to 7085ca1 Compare March 28, 2026 19:32
@miguel-heygen miguel-heygen force-pushed the refactor/shared-studio-api branch from f44a316 to faccb37 Compare March 28, 2026 19:37
@miguel-heygen miguel-heygen force-pushed the fix/lint-template-stripping branch 2 times, most recently from 3b76ee2 to 0a1a093 Compare March 28, 2026 19:42
@miguel-heygen miguel-heygen force-pushed the refactor/shared-studio-api branch from faccb37 to 229f62d Compare March 28, 2026 19:43
@miguel-heygen miguel-heygen force-pushed the fix/lint-template-stripping branch from 0a1a093 to d551be4 Compare March 28, 2026 19:47
@miguel-heygen miguel-heygen force-pushed the refactor/shared-studio-api branch from 229f62d to 31701fe Compare March 28, 2026 19:48
@miguel-heygen miguel-heygen force-pushed the fix/lint-template-stripping branch from d551be4 to 2a2a676 Compare March 28, 2026 19:52
@miguel-heygen miguel-heygen force-pushed the refactor/shared-studio-api branch from 31701fe to 69a4de3 Compare March 28, 2026 19:53
@miguel-heygen miguel-heygen force-pushed the fix/lint-template-stripping branch from 2a2a676 to 87a108d Compare March 28, 2026 19:57
@miguel-heygen miguel-heygen force-pushed the refactor/shared-studio-api branch from 69a4de3 to 8a2af77 Compare March 28, 2026 19:58
@miguel-heygen miguel-heygen force-pushed the fix/lint-template-stripping branch from 87a108d to 126b1b6 Compare March 28, 2026 19:59
@miguel-heygen miguel-heygen force-pushed the refactor/shared-studio-api branch from 8a2af77 to 029329e Compare March 28, 2026 19:59
@miguel-heygen miguel-heygen force-pushed the fix/lint-template-stripping branch from 126b1b6 to 327d0c5 Compare March 28, 2026 20:09
@miguel-heygen miguel-heygen force-pushed the refactor/shared-studio-api branch from 029329e to 9356f4e Compare March 28, 2026 20:09
@miguel-heygen miguel-heygen force-pushed the fix/lint-template-stripping branch from 327d0c5 to 348d260 Compare March 28, 2026 20:29
@miguel-heygen miguel-heygen force-pushed the refactor/shared-studio-api branch from 9356f4e to 37bbab3 Compare March 28, 2026 20:30
@miguel-heygen miguel-heygen force-pushed the fix/lint-template-stripping branch 2 times, most recently from 91c73ec to c807643 Compare March 28, 2026 20:49
@miguel-heygen miguel-heygen force-pushed the refactor/shared-studio-api branch from 37bbab3 to a57d95f Compare March 28, 2026 20:50
@miguel-heygen miguel-heygen changed the base branch from fix/lint-template-stripping to graphite-base/113 March 28, 2026 21:08
@miguel-heygen miguel-heygen force-pushed the refactor/shared-studio-api branch from a57d95f to 99f4d36 Compare March 28, 2026 21:08
@graphite-app graphite-app bot changed the base branch from graphite-base/113 to main March 28, 2026 21:09
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) <noreply@anthropic.com>
@miguel-heygen miguel-heygen force-pushed the refactor/shared-studio-api branch from 99f4d36 to 644ad12 Compare March 28, 2026 21:09
@miguel-heygen miguel-heygen merged commit bd175b6 into main Mar 28, 2026
22 checks passed
Copy link
Copy Markdown
Collaborator Author

Merge activity

@miguel-heygen miguel-heygen deleted the refactor/shared-studio-api branch April 6, 2026 23:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants