mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 12:54:29 +00:00
test(core): cover the group-bus routing, and clear the last five oversized files
Two loose ends from the rebase.
**The routing had no test.** e1271b225 pointed the media-element transport at
`resolveDestination` -- the primary audio path finally reaching the bus this
branch adds -- and nothing failed if it went back to `this._masterGain`. Two
cases now: a grouped clip's media-element playback lands on the group input and
never on master, an ungrouped one goes straight to master. Verified they FAIL on
a revert of that one line. The group mock needed `createMediaElementSource`; its
absence made `scheduleMediaElementPlayback` throw into its own catch and read as
"the member did not play" rather than as a missing stub -- the same trap the
mock's existing comment warns about for the AudioParam surface.
**Five studio files were over the 600-line cap.** All five were pushed over BY
this branch (main had them at 597, 572, 541, and under), so any future commit
touching one needed --no-verify -- the thing this stack set out to end:
- TimelineLanes.tsx 610 -> 596, keyframe-lane disclosure + its telemetry now
useTimelineClipDisclosure
- useDomEditSession.ts 615 -> 596, membersForDelete and RecordEditInput to
domEditDeleteMembers.ts (re-exported, its test imports from the old home)
- useTimelineEditing.ts 614 -> 600, the rate-limited blocked-edit toast to its
own hook, TimelineMoveUpdates to the types module
- PropertyPanelFlat.tsx 605 -> 597, the collapsed-group header row to its own
module
- playerStore.ts 604 -> 594, the dev-build console handle to its own module
Extracting in place made PropertyPanelFlat GROW (605 -> 616): a signature plus a
doc comment costs more than an inline arrow saves. Only a move to a sibling
module actually removes lines.
Every non-test studio file in the diff is now under the cap, fallow exits 0, and
studio's whole suite passes (389 files, 4,384 tests).
This commit is contained in:
@@ -642,6 +642,7 @@ describe("WebAudioTransport", () => {
|
||||
getFloatTimeDomainData: ReturnType<typeof vi.fn>;
|
||||
}[] = [];
|
||||
const masterGain = { gain: { value: 1 }, connect: vi.fn(), disconnect: vi.fn() };
|
||||
const mediaElementSource = { connect: vi.fn(), disconnect: vi.fn() };
|
||||
const ctx = {
|
||||
currentTime,
|
||||
state: "running",
|
||||
@@ -687,10 +688,14 @@ describe("WebAudioTransport", () => {
|
||||
analysers.push(node);
|
||||
return node;
|
||||
}),
|
||||
// The media-element route needs this as much as the decoded one: without
|
||||
// it `scheduleMediaElementPlayback` throws and its catch returns null,
|
||||
// which reads as "the member did not play" rather than a missing stub.
|
||||
createMediaElementSource: vi.fn(() => mediaElementSource),
|
||||
destination: {},
|
||||
close: vi.fn(),
|
||||
};
|
||||
return { ctx, gainNodes, analysers, masterGain };
|
||||
return { ctx, gainNodes, analysers, masterGain, mediaElementSource };
|
||||
}
|
||||
|
||||
function setupGroupTransport(currentTime = 100) {
|
||||
@@ -743,6 +748,30 @@ describe("WebAudioTransport", () => {
|
||||
expect(clipGain!.connect).toHaveBeenCalledWith(mock.masterGain);
|
||||
});
|
||||
|
||||
// The media-element transport is the PRIMARY path for audio — the runtime
|
||||
// tries it first and only falls back to a decoded buffer. It has to reach
|
||||
// the same bus, or grouping silently applies to nothing that actually plays.
|
||||
it("routes a grouped clip's MEDIA-ELEMENT playback to the group bus, not master", async () => {
|
||||
const { transport, mock, gen } = setupGroupTransport();
|
||||
const el = groupedAudioEl("vo-1", "vo");
|
||||
|
||||
await transport.scheduleMediaElementPlayback(el, 0, 0, 0, 1, gen, 1);
|
||||
|
||||
const clipGain = mock.gainNodes[0]!;
|
||||
expect(clipGain.connect).toHaveBeenCalledWith(firstGroupInput(mock));
|
||||
expect(clipGain.connect).not.toHaveBeenCalledWith(mock.masterGain);
|
||||
});
|
||||
|
||||
it("routes an UNGROUPED clip's media-element playback straight to master", async () => {
|
||||
const { transport, mock, gen } = setupGroupTransport();
|
||||
const el = groupedAudioEl("lone");
|
||||
|
||||
await transport.scheduleMediaElementPlayback(el, 0, 0, 0, 1, gen, 1);
|
||||
|
||||
expect(mock.gainNodes).toHaveLength(1);
|
||||
expect(mock.gainNodes[0]!.connect).toHaveBeenCalledWith(mock.masterGain);
|
||||
});
|
||||
|
||||
it("two members of the same group land on ONE shared group gain, not master directly", async () => {
|
||||
const { transport, mock, gen } = setupGroupTransport();
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { audioFxSummary } from "./audioFxSummary";
|
||||
import { HF_AUDIO_GROUP_TAG, resolveAudioGroups } from "@hyperframes/core/audio-groups";
|
||||
import { PropertyPanelFlatHeader } from "./PropertyPanelFlatHeader";
|
||||
import { PropertyPanelFlatFooter } from "./PropertyPanelFlatFooter";
|
||||
import { closedGroupHeader } from "./propertyPanelFlatClosedGroup";
|
||||
import { FlatGroupHeader } from "./propertyPanelFlatPrimitives";
|
||||
import { FlatTextSection } from "./propertyPanelFlatTextSection";
|
||||
import { FlatStyleSection } from "./propertyPanelFlatStyleSections";
|
||||
@@ -524,17 +525,8 @@ export function PropertyPanelFlat({
|
||||
const beforeOpen = openIndex === -1 ? groups : groups.slice(0, openIndex);
|
||||
const openGroup = openIndex === -1 ? null : groups[openIndex];
|
||||
const afterOpen = openIndex === -1 ? [] : groups.slice(openIndex + 1);
|
||||
const renderClosedGroup = (group: FlatGroupDescriptor) => (
|
||||
<DesignPanelInputProvider key={group.id} section={slugifyDesignInput(group.title)}>
|
||||
<FlatGroupHeader
|
||||
title={group.title}
|
||||
isOpen={false}
|
||||
onToggleOpen={() => toggleOpen(group.id)}
|
||||
summary={group.summary}
|
||||
animateEntrance={justToggledIds.includes(group.id)}
|
||||
/>
|
||||
</DesignPanelInputProvider>
|
||||
);
|
||||
const renderClosedGroup = (group: FlatGroupDescriptor) =>
|
||||
closedGroupHeader(group, toggleOpen, justToggledIds);
|
||||
|
||||
return (
|
||||
<DesignPanelInputProvider ui="flat">
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* A collapsed group's header row in the flat inspector.
|
||||
*
|
||||
* Its own module so `PropertyPanelFlat.tsx` stays under the studio's 600-line
|
||||
* cap; it reads only its arguments, which is what makes it separable.
|
||||
*/
|
||||
|
||||
import { DesignPanelInputProvider } from "../../contexts/DesignPanelInputContext";
|
||||
import { slugifyDesignInput } from "../../utils/designInputTracking";
|
||||
import { FlatGroupHeader } from "./propertyPanelFlatPrimitives";
|
||||
import type { FlatGroupDescriptor } from "./propertyPanelFlatDescriptors";
|
||||
|
||||
/** Its title, its one-line summary, and the entrance animation only the group
|
||||
* just toggled gets. */
|
||||
export function closedGroupHeader(
|
||||
group: FlatGroupDescriptor,
|
||||
toggleOpen: (id: string) => void,
|
||||
justToggledIds: readonly string[],
|
||||
) {
|
||||
return (
|
||||
<DesignPanelInputProvider key={group.id} section={slugifyDesignInput(group.title)}>
|
||||
<FlatGroupHeader
|
||||
title={group.title}
|
||||
isOpen={false}
|
||||
onToggleOpen={() => toggleOpen(group.id)}
|
||||
summary={group.summary}
|
||||
animateEntrance={justToggledIds.includes(group.id)}
|
||||
/>
|
||||
</DesignPanelInputProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Which elements a delete acts on.
|
||||
*
|
||||
* Its own module so `useDomEditSession.ts` stays under the studio's 600-line
|
||||
* cap; it reads only its arguments.
|
||||
*/
|
||||
|
||||
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
||||
import type { EditHistoryKind } from "../utils/editHistory";
|
||||
|
||||
/** One entry in the studio's edit history, as `useDomEditSession`'s caller
|
||||
* supplies it. */
|
||||
export interface RecordEditInput {
|
||||
label: string;
|
||||
kind: EditHistoryKind;
|
||||
coalesceKey?: string;
|
||||
files: Record<string, { before: string; after: string }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Which elements a delete acts on. `expandGroup` widens the primary to the
|
||||
* whole marquee group, which is what the Delete key means.
|
||||
*
|
||||
* The caller chooses rather than the delete deciding for everyone: Cut copies
|
||||
* the primary alone, so expanding for it put one element on the clipboard and
|
||||
* removed every other member of the group with it.
|
||||
*/
|
||||
export function membersForDelete(
|
||||
selection: DomEditSelection,
|
||||
group: DomEditSelection[],
|
||||
options?: { expandGroup?: boolean },
|
||||
): DomEditSelection[] {
|
||||
return options?.expandGroup && group.length > 0 ? group : [selection];
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* The "can't be moved from the timeline yet" toast, rate-limited.
|
||||
*
|
||||
* Its own hook so `useTimelineEditing.ts` stays under the studio's 600-line cap.
|
||||
* The 1.5s gate matters: a blocked drag fires this per pointermove, and without
|
||||
* it one gesture stacked dozens of identical toasts.
|
||||
*/
|
||||
|
||||
import { useCallback, useRef } from "react";
|
||||
import type { TimelineElement } from "../player";
|
||||
|
||||
const BLOCKED_TOAST_INTERVAL_MS = 1500;
|
||||
|
||||
export function useBlockedTimelineEditToast(
|
||||
showToast: (message: string, tone?: "info" | "error") => void,
|
||||
): (element: TimelineElement) => void {
|
||||
const lastAtRef = useRef(0);
|
||||
return useCallback(
|
||||
(_element: TimelineElement) => {
|
||||
const now = Date.now();
|
||||
if (now - lastAtRef.current < BLOCKED_TOAST_INTERVAL_MS) return;
|
||||
lastAtRef.current = now;
|
||||
showToast("This clip can't be moved or resized from the timeline yet.", "info");
|
||||
},
|
||||
[showToast],
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import { trackStudioEvent } from "../utils/studioTelemetry";
|
||||
import { isAudioDomElement } from "../utils/timelineInspector";
|
||||
import type { SelectElementOptions, TimelineElement } from "../player";
|
||||
import type { ImportedFontAsset } from "../components/editor/fontAssets";
|
||||
import type { EditHistoryKind } from "../utils/editHistory";
|
||||
import type { RightPanelTab } from "../utils/studioHelpers";
|
||||
import type { PatchTarget } from "../utils/sourcePatcher";
|
||||
import type { SidebarTab } from "../components/sidebar/LeftSidebar";
|
||||
@@ -22,13 +21,11 @@ import { useGsapAwareEditing } from "./useGsapAwareEditing";
|
||||
import { useStudioSelectionPublisher } from "./useStudioSelectionPublisher";
|
||||
import { useKeyframeEaseCommits } from "./useKeyframeEaseCommits";
|
||||
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
||||
|
||||
interface RecordEditInput {
|
||||
label: string;
|
||||
kind: EditHistoryKind;
|
||||
coalesceKey?: string;
|
||||
files: Record<string, { before: string; after: string }>;
|
||||
}
|
||||
import { membersForDelete } from "./domEditDeleteMembers";
|
||||
import type { RecordEditInput } from "./domEditDeleteMembers";
|
||||
// Re-exported: the delete rule lives in its own module now, and callers (and its
|
||||
// own test) have always imported it from here.
|
||||
export { membersForDelete };
|
||||
|
||||
export interface UseDomEditSessionParams {
|
||||
projectId: string | null;
|
||||
@@ -74,22 +71,6 @@ export interface UseDomEditSessionParams {
|
||||
forceReloadSdkSession?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Which elements a delete acts on. `expandGroup` widens the primary to the
|
||||
* whole marquee group, which is what the Delete key means.
|
||||
*
|
||||
* The caller chooses rather than the delete deciding for everyone: Cut copies
|
||||
* the primary alone, so expanding for it put one element on the clipboard and
|
||||
* removed every other member of the group with it.
|
||||
*/
|
||||
export function membersForDelete(
|
||||
selection: DomEditSelection,
|
||||
group: DomEditSelection[],
|
||||
options?: { expandGroup?: boolean },
|
||||
): DomEditSelection[] {
|
||||
return options?.expandGroup && group.length > 0 ? group : [selection];
|
||||
}
|
||||
|
||||
export function useDomEditSession({
|
||||
projectId,
|
||||
activeCompPath,
|
||||
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
syncPreviewContentDuration,
|
||||
} from "./timelineTimingSync";
|
||||
import type { PersistTimelineEditInput } from "./timelineEditingHelpers";
|
||||
import type { TimelineStackingReorderIntent } from "../player/components/timelineEditing";
|
||||
import { useSetAudioGroupAttribute } from "./timelineAudioGroupVolume";
|
||||
import { useSetElementAttribute } from "./timelineElementFxAttribute";
|
||||
import { useAudioGroupCarveAssignment } from "./timelineAudioGroupCreate";
|
||||
@@ -35,16 +34,13 @@ import {
|
||||
useTimelineTrackVisibilityEditing,
|
||||
} from "./timelineTrackVisibility";
|
||||
import { useTimelineGroupEditing } from "./useTimelineGroupEditing";
|
||||
import { useBlockedTimelineEditToast } from "./useBlockedTimelineEditToast";
|
||||
import { serializeZLaneGesture } from "../components/nle/zLaneGesture";
|
||||
import { cutoverCommittedOrThrow, sdkTimingPersist } from "../utils/sdkCutover";
|
||||
import type { UseTimelineEditingOptions } from "./useTimelineEditingTypes";
|
||||
import type { TimelineMoveUpdates, UseTimelineEditingOptions } from "./useTimelineEditingTypes";
|
||||
import { getStudioSaveErrorMessage } from "../utils/studioSaveDiagnostics";
|
||||
import { studioWriteHeaders } from "../utils/studioFileVersion";
|
||||
|
||||
type TimelineMoveUpdates = Pick<TimelineElement, "start" | "track"> & {
|
||||
stackingReorder?: TimelineStackingReorderIntent | null;
|
||||
};
|
||||
|
||||
export function useTimelineEditing({
|
||||
projectId,
|
||||
activeCompPath,
|
||||
@@ -67,9 +63,7 @@ export function useTimelineEditing({
|
||||
}: UseTimelineEditingOptions) {
|
||||
const projectIdRef = useRef(projectId);
|
||||
projectIdRef.current = projectId;
|
||||
|
||||
const editQueueRef = useRef(Promise.resolve());
|
||||
const lastBlockedTimelineToastAtRef = useRef(0);
|
||||
|
||||
const enqueueEdit = useCallback(
|
||||
(
|
||||
@@ -569,15 +563,7 @@ export function useTimelineEditing({
|
||||
observeProjectFileVersion,
|
||||
});
|
||||
|
||||
const handleBlockedTimelineEdit = useCallback(
|
||||
(_element: TimelineElement) => {
|
||||
const now = Date.now();
|
||||
if (now - lastBlockedTimelineToastAtRef.current < 1500) return;
|
||||
lastBlockedTimelineToastAtRef.current = now;
|
||||
showToast("This clip can't be moved or resized from the timeline yet.", "info");
|
||||
},
|
||||
[showToast],
|
||||
);
|
||||
const handleBlockedTimelineEdit = useBlockedTimelineEditToast(showToast);
|
||||
|
||||
const { handleRazorSplit, handleRazorSplitAll } = useRazorSplit({
|
||||
projectId,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { MutableRefObject, RefObject } from "react";
|
||||
import type { Composition } from "@hyperframes/sdk";
|
||||
import type { TimelineElement } from "../player";
|
||||
import type { TimelineStackingReorderIntent } from "../player/components/timelineEditing";
|
||||
import type { EditHistoryKind } from "../utils/editHistory";
|
||||
import type { PublishSdkSession } from "../utils/sdkCutover";
|
||||
|
||||
@@ -55,3 +56,9 @@ export type TimelineFileDropHandler = (
|
||||
files: File[],
|
||||
placement?: { start: number; track: number },
|
||||
) => Promise<void>;
|
||||
|
||||
/** What a timeline move commits: the new start and track, plus the z-index
|
||||
* reorder a vertical drag resolves to (absent for a pure horizontal move). */
|
||||
export type TimelineMoveUpdates = Pick<TimelineElement, "start" | "track"> & {
|
||||
stackingReorder?: TimelineStackingReorderIntent | null;
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useAutomationSelectionKeyboard } from "../../hooks/useAutomationSelecti
|
||||
import { TimelineTrackHeader } from "./TimelineTrackHeader";
|
||||
import { TimelineGroupRow } from "./TimelineGroupRow";
|
||||
import { useTimelineLaneRowIndexes, useTimelineGroupDisclosure } from "./useTimelineLaneRowIndexes";
|
||||
import { useTimelineClipDisclosure } from "./useTimelineClipDisclosure";
|
||||
import {
|
||||
isTrackRowExpanded,
|
||||
resolveTrackKeyframeClip,
|
||||
@@ -22,7 +23,6 @@ import { usePlayerStore } from "../store/playerStore";
|
||||
import { isMultiDragPassenger, multiDragPassengerOffsetPx } from "./timelineMultiDragPreview";
|
||||
import { useTimelineMultiDragActorWindows } from "./useTimelineMultiDragActorWindows";
|
||||
import type { TimelineLanesProps } from "./timelineLaneProps";
|
||||
import { trackStudioKeyframeLaneExpand } from "../../telemetry/events";
|
||||
import { isAudioTimelineElement, isMusicTrack } from "../../utils/timelineInspector";
|
||||
import { createClipGestureHandlers } from "./timelineClipGestureHandlers";
|
||||
import { renderClipChildren, resolveClipRenderContext } from "./timelineClipChildren";
|
||||
@@ -107,9 +107,6 @@ export function TimelineLanes({
|
||||
// synthetic lane element spans the whole composition rather than a clip.
|
||||
const compositionDuration = usePlayerStore((s) => s.duration);
|
||||
useAutomationSelectionKeyboard({ lanes: automationLanes });
|
||||
const expandClips = usePlayerStore((s) => s.expandClips);
|
||||
const setClipExpanded = usePlayerStore((s) => s.setClipExpanded);
|
||||
const toggleClipExpanded = usePlayerStore((s) => s.toggleClipExpanded);
|
||||
const { logicalRowsByTrack, groupByAnchor } = useTimelineLaneRowIndexes(logicalRows, groups);
|
||||
// Which tracks are group MEMBERS, so their headers can render the level-2
|
||||
// nesting their `aria-level` already reports.
|
||||
@@ -117,21 +114,10 @@ export function TimelineLanes({
|
||||
() => new Set(groups.flatMap((group) => group.memberTracks)),
|
||||
[groups],
|
||||
);
|
||||
// The caret belongs to the ROW, so it opens and closes every clip on it at
|
||||
// once. Toggling only the active clip left the row's state depending on which
|
||||
// sibling happened to be selected: expand one, click another, and the row
|
||||
// collapsed under a caret that still pointed down.
|
||||
const toggleRowExpandedTracked = (keys: readonly string[]) => {
|
||||
const willExpand = !keys.some((key) => expandedClipIds.has(key));
|
||||
trackStudioKeyframeLaneExpand({ expanded: willExpand });
|
||||
if (willExpand) expandClips(keys);
|
||||
else for (const key of keys) setClipExpanded(key, false);
|
||||
};
|
||||
const toggleClipExpandedTracked = (key: string) => {
|
||||
const willExpand = !expandedClipIds.has(key);
|
||||
trackStudioKeyframeLaneExpand({ expanded: willExpand });
|
||||
toggleClipExpanded(key);
|
||||
};
|
||||
const {
|
||||
toggleRowExpanded: toggleRowExpandedTracked,
|
||||
toggleClipExpanded: toggleClipExpandedTracked,
|
||||
} = useTimelineClipDisclosure();
|
||||
const actorWindows = useTimelineMultiDragActorWindows(
|
||||
multiDragPreview,
|
||||
rowsVirtualized,
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Opening and closing a track's keyframe property lanes, with the telemetry that
|
||||
* goes with it.
|
||||
*
|
||||
* Split out of `TimelineLanes.tsx` to keep that file under the studio's 600-line
|
||||
* cap. Both callbacks were already the only place the disclosure state and its
|
||||
* `keyframe_lane_expand` event were written together, which is what makes them a
|
||||
* seam rather than a shuffle.
|
||||
*/
|
||||
|
||||
import { usePlayerStore } from "../store/playerStore";
|
||||
import { trackStudioKeyframeLaneExpand } from "../../telemetry/events";
|
||||
|
||||
export interface TimelineClipDisclosure {
|
||||
/** The caret belongs to the ROW, so it opens and closes every clip on it at
|
||||
* once. Toggling only the active clip left the row's state depending on which
|
||||
* sibling happened to be selected: expand one, click another, and the row
|
||||
* collapsed under a caret that still pointed down. */
|
||||
toggleRowExpanded: (keys: readonly string[]) => void;
|
||||
toggleClipExpanded: (key: string) => void;
|
||||
}
|
||||
|
||||
export function useTimelineClipDisclosure(): TimelineClipDisclosure {
|
||||
const expandedClipIds = usePlayerStore((s) => s.expandedClipIds);
|
||||
const expandClips = usePlayerStore((s) => s.expandClips);
|
||||
const setClipExpanded = usePlayerStore((s) => s.setClipExpanded);
|
||||
const toggleClipExpanded = usePlayerStore((s) => s.toggleClipExpanded);
|
||||
|
||||
return {
|
||||
toggleRowExpanded: (keys) => {
|
||||
const willExpand = !keys.some((key) => expandedClipIds.has(key));
|
||||
trackStudioKeyframeLaneExpand({ expanded: willExpand });
|
||||
if (willExpand) expandClips(keys);
|
||||
else for (const key of keys) setClipExpanded(key, false);
|
||||
},
|
||||
toggleClipExpanded: (key) => {
|
||||
trackStudioKeyframeLaneExpand({ expanded: !expandedClipIds.has(key) });
|
||||
toggleClipExpanded(key);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { create } from "zustand";
|
||||
import { attachPlayerStoreDevHandle } from "./playerStoreDevHandle";
|
||||
import type { MusicBeatAnalysis } from "@hyperframes/core/beats";
|
||||
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
||||
import type { BeatEditState } from "../../utils/beatEditing";
|
||||
@@ -548,7 +549,7 @@ export const usePlayerStore = create<PlayerState>((set, get) => ({
|
||||
activeKeyframePct: null,
|
||||
motionPathArmed: false,
|
||||
focusedEaseSegment: null,
|
||||
revealedAudioFxTarget: null,
|
||||
revealedAudioFxTarget: null,
|
||||
}
|
||||
: { selectedElementId: id, selectedElementIds };
|
||||
}),
|
||||
@@ -590,15 +591,4 @@ export const usePlayerStore = create<PlayerState>((set, get) => ({
|
||||
reset: () => set(createTimelineResetState()),
|
||||
}));
|
||||
|
||||
function isDevBuild(): boolean {
|
||||
try {
|
||||
return import.meta.env.DEV === true;
|
||||
} catch {
|
||||
// Turbopack and other non-Vite bundlers may not provide import.meta.env.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (isDevBuild() && typeof window !== "undefined") {
|
||||
// Console handle for dumping live Studio state during bug-bash reproduction.
|
||||
(window as unknown as { __playerStore?: typeof usePlayerStore }).__playerStore = usePlayerStore;
|
||||
}
|
||||
attachPlayerStoreDevHandle(usePlayerStore);
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* A console handle on the live store, dev builds only.
|
||||
*
|
||||
* Split out of `playerStore.ts` to keep it under the studio's 600-line cap. The
|
||||
* dev check is its own function because `import.meta.env` is absent under
|
||||
* Turbopack and other non-Vite bundlers, where reading it throws.
|
||||
*/
|
||||
|
||||
function isDevBuild(): boolean {
|
||||
try {
|
||||
return import.meta.env.DEV === true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Expose `store` as `window.__playerStore` for dumping live Studio state
|
||||
* during bug-bash reproduction. No-op outside a dev build or a browser. */
|
||||
export function attachPlayerStoreDevHandle(store: unknown): void {
|
||||
if (!isDevBuild() || typeof window === "undefined") return;
|
||||
(window as unknown as { __playerStore?: unknown }).__playerStore = store;
|
||||
}
|
||||
Reference in New Issue
Block a user