fix(ci): scope LOC check to studio, split useTimelinePlayer + hyperframes-player under 500 LOC (#750)

* refactor: split useTimelinePlayer.ts and hyperframes-player.ts into focused modules (<500 LOC each)

* fix(ci): scope 500 LOC check to packages/studio, add allowlist for grandfathered files

* feat(cli): Linux ARM64 support — auto-install Chromium on DGX Spark / GB10 / Jetson

Chrome Headless Shell has no Linux ARM64 binary. On arm64 Linux:
- Detects the platform automatically
- Tries to auto-install system Chromium via apt-get (works on Ubuntu/Debian ARM)
- Falls back to clear manual instructions with exact commands
- 'hyperframes browser ensure' guides through the setup interactively
- After setup, all render commands work without any flags

* fix(ci): disable Windows Defender real-time monitoring to prevent EPERM builds

Path exclusions are insufficient — Defender re-scans new files created
during bun install before the exclusion takes effect. Disable real-time
monitoring for the entire job duration instead (standard CI practice).

* refactor(studio): split all files >500 LOC + extract useToast, delete allowlist

All 11 large files split into focused modules under 500 LOC.
App.tsx extracted toast logic into useToast hook (493 LOC now).
.filesize-allowlist deleted — no longer needed.

* fix: remove unused imports from split files, extract useToast from App.tsx

App.tsx: 504 → 493 lines (toast logic extracted to useToast hook)
timelineDOM.ts: remove unused imports from re-export pattern
MotionPanel.tsx: remove unused clampStudioCustomEasePoints import
studioMotionOps.ts: remove unused StudioGsapMotionDirection import

* fix(ci): use Set-MpPreference to fully disable Windows Defender (both jobs)

* fix(producer): use node --experimental-strip-types instead of tsx for build:fonts

Eliminates the tsx binary dependency that Windows Defender locks during
bun install, causing EPERM errors. Node 22.6+ strips TypeScript types
natively with no external binary.

* chore: remove .filesize-allowlist — App.tsx is now 493 lines (<500)

* fix(ci): disable Windows Defender before checkout to prevent all EPERM races

* fix(producer): skip build:fonts if fontData.generated.ts already exists

The generated file is tracked in git, so CI doesn't need to regenerate
it. This avoids @fontsource/inter node_modules access on Windows which
triggers EPERM from Defender scanning during bun install.
This commit is contained in:
Miguel Ángel
2026-05-13 01:48:12 +02:00
committed by GitHub
parent 03475d54c6
commit 91bdffffe6
74 changed files with 11760 additions and 9759 deletions
@@ -0,0 +1,145 @@
/**
* Playback adapter utilities: factory for the static-seek adapter used when a
* composition exposes only a `renderSeek` / `seek` API (no native play/pause
* support), plus a thin wrapper that normalises GSAP-style `TimelineLike`
* objects to the `PlaybackAdapter` interface.
*/
import type {
PlaybackAdapter,
RuntimePlaybackAdapter,
StaticSeekPlaybackClock,
TimelineLike,
} from "./playbackTypes";
// ---------------------------------------------------------------------------
// Pure numeric helpers
// ---------------------------------------------------------------------------
export function isFinitePositive(value: number): boolean {
return Number.isFinite(value) && value > 0;
}
export function clampTime(time: number, duration: number): number {
const safeDuration = Math.max(0, Number.isFinite(duration) ? duration : 0);
const safeTime = Math.max(0, Number.isFinite(time) ? time : 0);
return safeDuration > 0 ? Math.min(safeTime, safeDuration) : safeTime;
}
export function getAdapterDuration(adapter: PlaybackAdapter | null | undefined): number {
if (!adapter) return 0;
try {
const duration = Number(adapter.getDuration());
return isFinitePositive(duration) ? duration : 0;
} catch {
return 0;
}
}
// ---------------------------------------------------------------------------
// Clock factory
// ---------------------------------------------------------------------------
export function getDefaultStaticSeekPlaybackClock(win: Window): StaticSeekPlaybackClock {
return {
now: () => win.performance.now(),
requestAnimationFrame: (callback) => win.requestAnimationFrame(callback),
cancelAnimationFrame: (handle) => win.cancelAnimationFrame(handle),
};
}
// ---------------------------------------------------------------------------
// Static-seek adapter
// ---------------------------------------------------------------------------
/**
* Wraps a render-only player (exposes `renderSeek`/`seek` but no native
* play/pause) and drives playback via `requestAnimationFrame`.
*/
export function createStaticSeekPlaybackAdapter(
player: Pick<RuntimePlaybackAdapter, "getTime"> &
Partial<Pick<RuntimePlaybackAdapter, "renderSeek" | "seek">>,
duration: number,
clock: StaticSeekPlaybackClock,
getPlaybackRate: () => number = () => 1,
): PlaybackAdapter {
const safeDuration = Math.max(0, Number.isFinite(duration) ? duration : 0);
let currentTime = clampTime(Number(player.getTime?.() ?? 0), safeDuration);
let playing = false;
let rafId = 0;
let playStartTime = currentTime;
let playStartNow = clock.now();
const renderSeek = (time: number) => {
currentTime = clampTime(time, safeDuration);
if (typeof player.renderSeek === "function") {
player.renderSeek(currentTime);
return;
}
player.seek?.(currentTime);
};
const stopTicker = () => {
if (rafId) {
clock.cancelAnimationFrame(rafId);
rafId = 0;
}
};
const tick: FrameRequestCallback = (now) => {
if (!playing) return;
const playbackRate = Math.max(0.1, Number(getPlaybackRate()) || 1);
const elapsed = ((now - playStartNow) / 1000) * playbackRate;
renderSeek(playStartTime + elapsed);
if (currentTime >= safeDuration) {
playing = false;
rafId = 0;
return;
}
rafId = clock.requestAnimationFrame(tick);
};
return {
play: () => {
if (playing || safeDuration <= 0) return;
if (currentTime >= safeDuration) renderSeek(0);
playing = true;
playStartTime = currentTime;
playStartNow = clock.now();
stopTicker();
rafId = clock.requestAnimationFrame(tick);
},
pause: () => {
playing = false;
stopTicker();
},
seek: (time) => {
renderSeek(time);
if (playing) {
playStartTime = currentTime;
playStartNow = clock.now();
}
},
getTime: () => currentTime,
getDuration: () => safeDuration,
isPlaying: () => playing,
};
}
// ---------------------------------------------------------------------------
// GSAP timeline wrapper
// ---------------------------------------------------------------------------
export function wrapTimeline(tl: TimelineLike): PlaybackAdapter {
return {
play: () => tl.play(),
pause: () => tl.pause(),
seek: (t) => {
tl.pause();
tl.seek(t);
},
getTime: () => tl.time(),
getDuration: () => tl.duration(),
isPlaying: () => tl.isActive(),
};
}