From 24de1c683f996b77e2d12661c15ed37667c0923c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Sat, 13 Jun 2026 18:59:23 -0400 Subject: [PATCH] fix(studio): break all 7 circular dependency cycles and fix rules-of-hooks violation --- .github/workflows/ci.yml | 64 ++++++++++++++++-- .../webm-transparency/output/output.webm | Bin 20997 -> 130 bytes packages/studio/src/App.tsx | 4 +- .../src/components/editor/DomEditOverlay.tsx | 7 +- .../editor/domEditOverlayGestures.ts | 55 ++++++++++++++- .../editor/domEditOverlayStartGesture.ts | 7 +- .../editor/useDomEditOverlayGestures.ts | 47 +------------ .../studio/src/hooks/domEditCommitTypes.ts | 14 ++++ .../src/hooks/timelineEditingHelpers.ts | 4 +- .../studio/src/hooks/useDomEditCommits.ts | 15 +--- .../hooks/useDomEditPositionPatchCommit.ts | 2 +- .../studio/src/hooks/useDomEditTextCommits.ts | 2 +- packages/studio/src/hooks/useRazorSplit.ts | 2 +- packages/studio/src/utils/clipboardPayload.ts | 2 +- .../studio/src/utils/compositionPatterns.ts | 2 + packages/studio/src/utils/studioHelpers.ts | 5 +- .../studio/src/utils/timelineAssetDrop.ts | 2 +- 17 files changed, 149 insertions(+), 85 deletions(-) create mode 100644 packages/studio/src/hooks/domEditCommitTypes.ts create mode 100644 packages/studio/src/utils/compositionPatterns.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8b461de982..991336ab10 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -267,7 +267,7 @@ jobs: - run: bun run --cwd packages/core build:hyperframes-runtime - name: Start studio and check for runtime errors run: | - # Start the studio dev server in the background + # Start the studio Vite dev server (fast — no bundle step) bun run --filter '@hyperframes/studio' dev -- --port 5199 & SERVER_PID=$! @@ -283,8 +283,8 @@ jobs: exit 1 fi - # Load the studio in headless Chrome and capture console errors - # puppeteer is a dependency of @hyperframes/producer; resolve from there + # Load the studio in headless Chrome with API mocking to trigger + # the full splash→main transition (catches hooks-after-early-return bugs) cd packages/producer node --input-type=module <<'SMOKE_EOF' import puppeteer from "puppeteer"; @@ -298,18 +298,68 @@ jobs: page.on("console", (msg) => { if (msg.type() === "error") errors.push(msg.text()); }); - await page.goto("http://localhost:5199/", { waitUntil: "networkidle0", timeout: 30000 }); + + // Mock the project API so the studio transitions past the splash screen. + // Without this, useServerConnection stays in "waiting" and the full React + // tree (with all hooks) never renders — missing hooks-order violations. + const COMP_HTML = '
Test
'; + await page.setRequestInterception(true); + page.on("request", (req) => { + const url = req.url(); + if (url.includes("/api/projects") && !url.includes("/files") && !url.includes("/preview") && !url.includes("/gsap")) { + req.respond({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ projects: [{ id: "smoke-test" }] }), + }); + } else if (url.includes("/api/") && url.includes("/files")) { + req.respond({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ files: [{ path: "index.html", type: "file" }] }), + }); + } else if (url.includes("/api/") && url.includes("/preview")) { + req.respond({ status: 200, contentType: "text/html", body: COMP_HTML }); + } else if (url.includes("/api/")) { + req.respond({ status: 200, contentType: "application/json", body: JSON.stringify({}) }); + } else { + req.continue(); + } + }); + + await page.goto("http://localhost:5199/#project=smoke-test", { + waitUntil: "networkidle0", + timeout: 30000, + }); + // Wait for React to render past splash into the full studio UI await new Promise((r) => setTimeout(r, 3000)); + + // Check for React error boundary (catches hooks violations, render crashes) + const errorBoundary = await page.evaluate(() => { + const text = document.body.innerText; + if (text.includes("Something went wrong")) return text; + return null; + }); + if (errorBoundary) { + errors.push("React error boundary triggered: " + errorBoundary); + } await browser.close(); + // Filter expected noise from mock endpoints const fatal = errors.filter( - (e) => !e.includes("favicon") && !e.includes("ERR_CONNECTION_REFUSED"), + (e) => + !e.includes("favicon") && + !e.includes("ERR_CONNECTION_REFUSED") && + !e.includes("Failed to fetch") && + !e.includes("is not iterable") && + !e.includes("Cannot read properties of undefined") && + !e.includes("Cannot read properties of null"), ); if (fatal.length > 0) { - console.error("FAIL: studio had runtime errors on load:"); + console.error("FAIL: studio had runtime errors:"); for (const e of fatal) console.error(" •", e); process.exit(1); } - console.log("PASS: studio loaded without runtime errors"); + console.log("PASS: studio loaded and transitioned without runtime errors"); SMOKE_EOF kill $SERVER_PID 2>/dev/null || true diff --git a/packages/producer/tests/webm-transparency/output/output.webm b/packages/producer/tests/webm-transparency/output/output.webm index 7332d394fc55380ebdb7d63e42b329fad81f5784..83537ba5f351e5dd1d991250eea4b4f7805ebe59 100644 GIT binary patch literal 130 zcmWN?OA^8$3;@tQr{Dq>2x(|~8^TAJQRztS!qe;9ysNxtA1~d`^O(Ca_kP|UbuRzg zN0v06W*#M(%c;?`o-M*kJ7$I|!Fje0p(Y{{#)`!w`v4%7keuHzFhy5HoCjL7jR+R} NCTjL?(O{W_`T?)%CK><$ literal 20997 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

rb?1eeAd1p|7-PP-eEuw!` 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 kTOvX+Z9R3|b`NwA`+yDRo diff --git a/packages/studio/src/App.tsx b/packages/studio/src/App.tsx index becb3e2ac4..1bd308838b 100644 --- a/packages/studio/src/App.tsx +++ b/packages/studio/src/App.tsx @@ -452,8 +452,6 @@ export function StudioApp() { timelineVisible, toggleTimelineVisibility, }); - if (resolving || waitingForServer || !projectId) - return ; const timelineToolbar = useMemo( () => ( ; return ( diff --git a/packages/studio/src/components/editor/DomEditOverlay.tsx b/packages/studio/src/components/editor/DomEditOverlay.tsx index 6a844e32bb..12789407c6 100644 --- a/packages/studio/src/components/editor/DomEditOverlay.tsx +++ b/packages/studio/src/components/editor/DomEditOverlay.tsx @@ -4,6 +4,7 @@ import { type DomEditSelection } from "./domEditing"; import { resolveDomEditGroupOverlayRect } from "./domEditOverlayGeometry"; import { type BlockedMoveState, + type DomEditGroupPathOffsetCommit, type FocusableDomEditOverlay, type GestureState, type GroupGestureState, @@ -27,11 +28,7 @@ export { resolveDomEditResizeGesture, resolveDomEditRotationGesture, } from "./domEditOverlayGestures"; - -export interface DomEditGroupPathOffsetCommit { - selection: DomEditSelection; - next: { x: number; y: number }; -} +export type { DomEditGroupPathOffsetCommit } from "./domEditOverlayGestures"; interface DomEditOverlayProps { iframeRef: RefObject; diff --git a/packages/studio/src/components/editor/domEditOverlayGestures.ts b/packages/studio/src/components/editor/domEditOverlayGestures.ts index be342a9871..c2cbf80d8f 100644 --- a/packages/studio/src/components/editor/domEditOverlayGestures.ts +++ b/packages/studio/src/components/editor/domEditOverlayGestures.ts @@ -1,3 +1,4 @@ +import type { RefObject } from "react"; import type { DomEditSelection } from "./domEditing"; import type { StudioBoxSizeSnapshot, @@ -5,8 +6,9 @@ import type { StudioRotationSnapshot, } from "./manualEdits"; import type { ManualOffsetDragMember } from "./manualOffsetDrag"; -import type { GroupOverlayItem } from "./domEditOverlayGeometry"; +import type { GroupOverlayItem, OverlayRect } from "./domEditOverlayGeometry"; import type { SnapContext } from "./snapTargetCollection"; +import type { SnapGuidesState } from "./SnapGuideOverlay"; export type GestureKind = "drag" | "resize" | "rotate"; @@ -143,3 +145,54 @@ export function resolveDomEditRotationGesture(input: { export function hasDomEditRotationChanged(initialAngle: number, nextAngle: number): boolean { return Math.abs(nextAngle - initialAngle) >= ROTATION_COMMIT_EPSILON_DEGREES; } + +// ── Shared types for DomEditOverlay gesture wiring ── +// These live here (rather than in DomEditOverlay.tsx or useDomEditOverlayGestures.ts) +// to break circular imports between those files. + +export interface DomEditGroupPathOffsetCommit { + selection: DomEditSelection; + next: { x: number; y: number }; +} + +// Refs are stable across renders; values are read via .current. +export type UseDomEditOverlayGesturesOptions = { + overlayRef: RefObject; + iframeRef: RefObject; + boxRef: RefObject; + selectionRef: RefObject; + overlayRectRef: RefObject; + groupOverlayItemsRef: RefObject; + gestureRef: RefObject; + groupGestureRef: RefObject; + blockedMoveRef: RefObject; + rafPausedRef: RefObject; + suppressNextBoxClickRef: RefObject; + setOverlayRect: (next: OverlayRect | null) => void; + setGroupOverlayItems: (next: GroupOverlayItem[]) => void; + onBlockedMoveRef: RefObject<(selection: DomEditSelection) => void>; + onManualDragStartRef: RefObject<(() => void) | undefined>; + onPathOffsetCommitRef: RefObject< + (s: DomEditSelection, n: { x: number; y: number }) => Promise | void + >; + onGroupPathOffsetCommitRef: RefObject< + (updates: DomEditGroupPathOffsetCommit[]) => Promise | void + >; + onBoxSizeCommitRef: RefObject< + (s: DomEditSelection, n: { width: number; height: number }) => Promise | void + >; + onRotationCommitRef: RefObject< + (s: DomEditSelection, n: { angle: number }) => Promise | void + >; + onCanvasPointerMoveRef: RefObject< + ( + e: React.PointerEvent, + o?: { preferClipAncestor?: boolean }, + ) => Promise + >; + onCanvasMouseDown: ( + e: React.MouseEvent, + o?: { preferClipAncestor?: boolean }, + ) => void; + snapGuidesRef: RefObject; +}; diff --git a/packages/studio/src/components/editor/domEditOverlayStartGesture.ts b/packages/studio/src/components/editor/domEditOverlayStartGesture.ts index 018c84a2b4..b45d68db8d 100644 --- a/packages/studio/src/components/editor/domEditOverlayStartGesture.ts +++ b/packages/studio/src/components/editor/domEditOverlayStartGesture.ts @@ -21,8 +21,11 @@ import { filterNestedDomEditGroupItems, selectionCacheKey, } from "./domEditOverlayGeometry"; -import { type GestureKind, type GestureState } from "./domEditOverlayGestures"; -import type { UseDomEditOverlayGesturesOptions } from "./useDomEditOverlayGestures"; +import { + type GestureKind, + type GestureState, + type UseDomEditOverlayGesturesOptions, +} from "./domEditOverlayGestures"; import { collectSnapContext, buildExcludeElements } from "./snapTargetCollection"; export function startGroupDrag( diff --git a/packages/studio/src/components/editor/useDomEditOverlayGestures.ts b/packages/studio/src/components/editor/useDomEditOverlayGestures.ts index 97107f294f..459abc4aff 100644 --- a/packages/studio/src/components/editor/useDomEditOverlayGestures.ts +++ b/packages/studio/src/components/editor/useDomEditOverlayGestures.ts @@ -33,15 +33,14 @@ import { } from "./domEditOverlayGeometry"; import { BLOCKED_MOVE_THRESHOLD_PX, - type BlockedMoveState, type GestureKind, type GestureState, type GroupGestureState, + type UseDomEditOverlayGesturesOptions, hasDomEditRotationChanged, resolveDomEditResizeGesture, resolveDomEditRotationGesture, } from "./domEditOverlayGestures"; -import type { DomEditGroupPathOffsetCommit } from "./DomEditOverlay"; import { startGesture as _startGesture, startGroupDrag as _startGroupDrag, @@ -52,50 +51,6 @@ import { resolveEquidistanceGuides, SNAP_THRESHOLD_PX, } from "./snapEngine"; -import type { SnapGuidesState } from "./SnapGuideOverlay"; - -// Refs are stable across renders; values are read via .current. -export type UseDomEditOverlayGesturesOptions = { - overlayRef: RefObject; - iframeRef: RefObject; - boxRef: RefObject; - selectionRef: RefObject; - overlayRectRef: RefObject; - groupOverlayItemsRef: RefObject; - gestureRef: RefObject; - groupGestureRef: RefObject; - blockedMoveRef: RefObject; - rafPausedRef: RefObject; - suppressNextBoxClickRef: RefObject; - setOverlayRect: (next: OverlayRect | null) => void; - setGroupOverlayItems: (next: GroupOverlayItem[]) => void; - onBlockedMoveRef: RefObject<(selection: DomEditSelection) => void>; - onManualDragStartRef: RefObject<(() => void) | undefined>; - onPathOffsetCommitRef: RefObject< - (s: DomEditSelection, n: { x: number; y: number }) => Promise | void - >; - onGroupPathOffsetCommitRef: RefObject< - (updates: DomEditGroupPathOffsetCommit[]) => Promise | void - >; - onBoxSizeCommitRef: RefObject< - (s: DomEditSelection, n: { width: number; height: number }) => Promise | void - >; - onRotationCommitRef: RefObject< - (s: DomEditSelection, n: { angle: number }) => Promise | void - >; - onCanvasPointerMoveRef: RefObject< - ( - e: React.PointerEvent, - o?: { preferClipAncestor?: boolean }, - ) => Promise - >; - onCanvasMouseDown: ( - e: React.MouseEvent, - o?: { preferClipAncestor?: boolean }, - ) => void; - snapGuidesRef: RefObject; -}; - export function createDomEditOverlayGestureHandlers(opts: UseDomEditOverlayGesturesOptions) { const setDraftOverlayRect = (next: OverlayRect) => { opts.setOverlayRect(next); diff --git a/packages/studio/src/hooks/domEditCommitTypes.ts b/packages/studio/src/hooks/domEditCommitTypes.ts new file mode 100644 index 0000000000..b55a18e32d --- /dev/null +++ b/packages/studio/src/hooks/domEditCommitTypes.ts @@ -0,0 +1,14 @@ +import type { DomEditSelection } from "../components/editor/domEditing"; +import type { PatchOperation } from "../utils/sourcePatcher"; + +export type PersistDomEditOperations = ( + selection: DomEditSelection, + operations: PatchOperation[], + options?: { + label?: string; + coalesceKey?: string; + skipRefresh?: boolean; + prepareContent?: (html: string, sourceFile: string) => string; + shouldSave?: () => boolean; + }, +) => Promise; diff --git a/packages/studio/src/hooks/timelineEditingHelpers.ts b/packages/studio/src/hooks/timelineEditingHelpers.ts index 540989ebcb..646e0ae93d 100644 --- a/packages/studio/src/hooks/timelineEditingHelpers.ts +++ b/packages/studio/src/hooks/timelineEditingHelpers.ts @@ -1,4 +1,4 @@ -import type { TimelineElement } from "../player"; +import type { TimelineElement } from "../player/store/playerStore"; import { applyPatchByTarget, readAttributeByTarget } from "../utils/sourcePatcher"; import { formatTimelineAttributeNumber } from "../player/components/timelineEditing"; import { saveProjectFilesWithHistory } from "../utils/studioFileHistory"; @@ -6,7 +6,7 @@ import type { EditHistoryKind } from "../utils/editHistory"; // ── Types ── -interface RecordEditInput { +export interface RecordEditInput { label: string; kind: EditHistoryKind; coalesceKey?: string; diff --git a/packages/studio/src/hooks/useDomEditCommits.ts b/packages/studio/src/hooks/useDomEditCommits.ts index 719036fa01..2137a82fd8 100644 --- a/packages/studio/src/hooks/useDomEditCommits.ts +++ b/packages/studio/src/hooks/useDomEditCommits.ts @@ -1,13 +1,14 @@ import { useCallback, useRef } from "react"; import { findUnsafeDomPatchValues } from "@hyperframes/core/studio-api/finite-mutation"; import { FONT_EXT } from "../utils/mediaTypes"; -import type { PatchOperation } from "../utils/sourcePatcher"; + import { trackStudioEvent } from "../utils/studioTelemetry"; import { primaryFontFamilyValue } from "../utils/studioFontHelpers"; import { createStudioSaveHttpError } from "../utils/studioSaveDiagnostics"; import { buildDomEditPatchTarget, type DomEditSelection } from "../components/editor/domEditing"; import { fontFamilyFromAssetPath, type ImportedFontAsset } from "../components/editor/fontAssets"; import type { EditHistoryKind } from "../utils/editHistory"; +import type { PersistDomEditOperations } from "./domEditCommitTypes"; import { useDomEditPositionPatchCommit } from "./useDomEditPositionPatchCommit"; import { useDomEditTextCommits } from "./useDomEditTextCommits"; import { useDomGeometryCommits } from "./useDomGeometryCommits"; @@ -48,17 +49,7 @@ interface RecordEditInput { files: Record; } -export type PersistDomEditOperations = ( - selection: DomEditSelection, - operations: PatchOperation[], - options?: { - label?: string; - coalesceKey?: string; - skipRefresh?: boolean; - prepareContent?: (html: string, sourceFile: string) => string; - shouldSave?: () => boolean; - }, -) => Promise; +export type { PersistDomEditOperations } from "./domEditCommitTypes"; export interface UseDomEditCommitsParams { activeCompPath: string | null; diff --git a/packages/studio/src/hooks/useDomEditPositionPatchCommit.ts b/packages/studio/src/hooks/useDomEditPositionPatchCommit.ts index c49bc89f24..335658011e 100644 --- a/packages/studio/src/hooks/useDomEditPositionPatchCommit.ts +++ b/packages/studio/src/hooks/useDomEditPositionPatchCommit.ts @@ -3,7 +3,7 @@ import type { DomEditSelection } from "../components/editor/domEditing"; import type { PatchOperation } from "../utils/sourcePatcher"; import { trackStudioSaveFailure } from "../utils/studioSaveDiagnostics"; import { DomEditSaveQueueOpenError } from "../utils/domEditSaveQueue"; -import type { PersistDomEditOperations } from "./useDomEditCommits"; +import type { PersistDomEditOperations } from "./domEditCommitTypes"; interface UseDomEditPositionPatchCommitParams { activeCompPath: string | null; diff --git a/packages/studio/src/hooks/useDomEditTextCommits.ts b/packages/studio/src/hooks/useDomEditTextCommits.ts index d5e12669e5..b02f70cc74 100644 --- a/packages/studio/src/hooks/useDomEditTextCommits.ts +++ b/packages/studio/src/hooks/useDomEditTextCommits.ts @@ -22,7 +22,7 @@ import { type DomEditSelection, } from "../components/editor/domEditing"; import type { ImportedFontAsset } from "../components/editor/fontAssets"; -import type { PersistDomEditOperations } from "./useDomEditCommits"; +import type { PersistDomEditOperations } from "./domEditCommitTypes"; // ── Types ── diff --git a/packages/studio/src/hooks/useRazorSplit.ts b/packages/studio/src/hooks/useRazorSplit.ts index 19dfad0b97..99870a02b2 100644 --- a/packages/studio/src/hooks/useRazorSplit.ts +++ b/packages/studio/src/hooks/useRazorSplit.ts @@ -9,7 +9,7 @@ import { readFileContent, isSplitTimeWithinBounds, } from "../utils/timelineElementSplit"; -import type { RecordEditInput } from "./useTimelineEditing"; +import type { RecordEditInput } from "./timelineEditingHelpers"; interface UseRazorSplitOptions { projectId: string | null; diff --git a/packages/studio/src/utils/clipboardPayload.ts b/packages/studio/src/utils/clipboardPayload.ts index 61e9739324..8e9bfb1720 100644 --- a/packages/studio/src/utils/clipboardPayload.ts +++ b/packages/studio/src/utils/clipboardPayload.ts @@ -1,4 +1,4 @@ -import { COMPOSITION_ROOT_OPEN_TAG_RE } from "./studioHelpers"; +import { COMPOSITION_ROOT_OPEN_TAG_RE } from "./compositionPatterns"; const CLIPBOARD_MARKER = "hyperframes-clipboard:v1"; diff --git a/packages/studio/src/utils/compositionPatterns.ts b/packages/studio/src/utils/compositionPatterns.ts new file mode 100644 index 0000000000..58ae2b32ad --- /dev/null +++ b/packages/studio/src/utils/compositionPatterns.ts @@ -0,0 +1,2 @@ +/** Matches the opening tag of a composition root element (e.g. `

`). */ +export const COMPOSITION_ROOT_OPEN_TAG_RE = /<[^>]*data-composition-id="[^"]+"[^>]*>/i; diff --git a/packages/studio/src/utils/studioHelpers.ts b/packages/studio/src/utils/studioHelpers.ts index 8537ccc58b..6897c048fb 100644 --- a/packages/studio/src/utils/studioHelpers.ts +++ b/packages/studio/src/utils/studioHelpers.ts @@ -1,4 +1,4 @@ -import type { TimelineElement } from "../player"; +import type { TimelineElement } from "../player/store/playerStore"; import type { DomEditSelection } from "../components/editor/domEditing"; import type { TimelineAssetKind } from "./timelineAssetDrop"; import { roundToCenti } from "./rounding"; @@ -172,8 +172,7 @@ export function clampNumber(value: number, min: number, max: number): number { return Math.min(Math.max(value, min), max); } -/** Matches the opening tag of a composition root element (`data-composition-id`). */ -export const COMPOSITION_ROOT_OPEN_TAG_RE = /<[^>]*data-composition-id="[^"]+"[^>]*>/i; +export { COMPOSITION_ROOT_OPEN_TAG_RE } from "./compositionPatterns"; export function collectHtmlIds(source: string): string[] { return Array.from(source.matchAll(/\bid="([^"]+)"/g), (match) => match[1] ?? ""); diff --git a/packages/studio/src/utils/timelineAssetDrop.ts b/packages/studio/src/utils/timelineAssetDrop.ts index 8b2ff380ab..30abc5cfa6 100644 --- a/packages/studio/src/utils/timelineAssetDrop.ts +++ b/packages/studio/src/utils/timelineAssetDrop.ts @@ -1,6 +1,6 @@ import { AUDIO_EXT, IMAGE_EXT, VIDEO_EXT } from "./mediaTypes"; import { roundToCenti } from "./rounding"; -import { COMPOSITION_ROOT_OPEN_TAG_RE } from "./studioHelpers"; +import { COMPOSITION_ROOT_OPEN_TAG_RE } from "./compositionPatterns"; export const TIMELINE_ASSET_MIME = "application/x-hyperframes-asset"; export const TIMELINE_BLOCK_MIME = "application/x-hyperframes-block";