From 221bd0922f1f64f2170637ec0e46cee5ea62d827 Mon Sep 17 00:00:00 2001 From: rahim Date: Mon, 9 Feb 2026 16:10:25 +1100 Subject: [PATCH] refactor(packages): simplify `createPlayer` type signatures (#477) --- packages/core/src/dom/media/types.ts | 36 ++++++++ .../src/dom/store/features/feature.parts.ts | 7 +- packages/html/src/define/player/video.ts | 2 +- packages/html/src/player/create-player.ts | 33 +++++--- .../src/player/tests/create-player.test-d.ts | 84 +++++++++++++++++++ .../src/player/tests/create-player.test.ts | 6 +- packages/react/src/player/create-player.tsx | 39 +++++---- .../src/player/tests/create-player.test-d.tsx | 73 ++++++++++++++++ 8 files changed, 248 insertions(+), 32 deletions(-) create mode 100644 packages/html/src/player/tests/create-player.test-d.ts create mode 100644 packages/react/src/player/tests/create-player.test-d.tsx diff --git a/packages/core/src/dom/media/types.ts b/packages/core/src/dom/media/types.ts index 2d7dec11..c577542e 100644 --- a/packages/core/src/dom/media/types.ts +++ b/packages/core/src/dom/media/types.ts @@ -1,5 +1,15 @@ import type { AnySlice, Slice, Store, UnionSliceState } from '@videojs/store'; +import type { + MediaBufferState, + MediaFullscreenState, + MediaPictureInPictureState, + MediaPlaybackState, + MediaSourceState, + MediaTimeState, + MediaVolumeState, +} from '../../core/media/state'; + export interface Media extends HTMLMediaElement {} export interface MediaContainer extends HTMLElement {} @@ -18,3 +28,29 @@ export type AnyPlayerFeature = AnySlice; export type PlayerStore = Store>; export type AnyPlayerStore = Store; + +// ---------------------------------------- +// Feature Presets +// ---------------------------------------- + +export type VideoFeatures = [ + PlayerFeature, + PlayerFeature, + PlayerFeature, + PlayerFeature, + PlayerFeature, + PlayerFeature, + PlayerFeature, +]; + +export type AudioFeatures = [ + PlayerFeature, + PlayerFeature, + PlayerFeature, + PlayerFeature, + PlayerFeature, +]; + +export type VideoPlayerStore = PlayerStore; + +export type AudioPlayerStore = PlayerStore; diff --git a/packages/core/src/dom/store/features/feature.parts.ts b/packages/core/src/dom/store/features/feature.parts.ts index d54ad5cd..9142c770 100644 --- a/packages/core/src/dom/store/features/feature.parts.ts +++ b/packages/core/src/dom/store/features/feature.parts.ts @@ -1,3 +1,4 @@ +import type { AudioFeatures, VideoFeatures } from '../../media/types'; import { bufferFeature } from './buffer'; import { fullscreenFeature } from './fullscreen'; import { pipFeature } from './pip'; @@ -17,7 +18,7 @@ export { volumeFeature as volume, }; -export const video = [ +export const video: VideoFeatures = [ playbackFeature, volumeFeature, timeFeature, @@ -25,6 +26,6 @@ export const video = [ bufferFeature, fullscreenFeature, pipFeature, -] as const; +]; -export const audio = [playbackFeature, volumeFeature, timeFeature, sourceFeature, bufferFeature] as const; +export const audio: AudioFeatures = [playbackFeature, volumeFeature, timeFeature, sourceFeature, bufferFeature]; diff --git a/packages/html/src/define/player/video.ts b/packages/html/src/define/player/video.ts index 7bb7b97b..d79a4807 100644 --- a/packages/html/src/define/player/video.ts +++ b/packages/html/src/define/player/video.ts @@ -3,7 +3,7 @@ import { MediaElement } from '@/ui/media-element'; import { createPlayer } from '../../player/create-player'; const { PlayerMixin } = createPlayer({ - features: [...features.video], + features: features.video, }); export class VideoPlayer extends PlayerMixin(MediaElement) {} diff --git a/packages/html/src/player/create-player.ts b/packages/html/src/player/create-player.ts index 9176c726..0c058f59 100644 --- a/packages/html/src/player/create-player.ts +++ b/packages/html/src/player/create-player.ts @@ -1,4 +1,12 @@ -import type { AnyPlayerFeature, PlayerStore, PlayerTarget } from '@videojs/core/dom'; +import type { + AnyPlayerFeature, + AudioFeatures, + AudioPlayerStore, + PlayerStore, + PlayerTarget, + VideoFeatures, + VideoPlayerStore, +} from '@videojs/core/dom'; import { combine, createStore } from '@videojs/store'; import { type ContainerMixin, createContainerMixin } from '../store/container-mixin'; @@ -45,7 +53,7 @@ export interface CreatePlayerResult { * import { createPlayer, MediaElement } from '@videojs/html'; * * const { PlayerElement, PlayerController, context } = createPlayer({ - * features: [...features.video], + * features: features.video, * }); * * // Simple: register pre-composed PlayerElement @@ -60,23 +68,26 @@ export interface CreatePlayerResult { * } * ``` */ +export function createPlayer(config: CreatePlayerConfig): CreatePlayerResult; + +export function createPlayer(config: CreatePlayerConfig): CreatePlayerResult; + export function createPlayer( config: CreatePlayerConfig -): CreatePlayerResult> { - type Store = PlayerStore; +): CreatePlayerResult>; - const slice = combine(...config.features); +export function createPlayer(config: CreatePlayerConfig): CreatePlayerResult { + const slice = combine(...config.features); - function create(): Store { + function create(): PlayerStore { return createStore()(slice); } - const ctx = playerContext as PlayerContext; - - const PlayerMixin = createPlayerMixin(ctx, create); + const ctx = playerContext; + const PlayerMixin = createPlayerMixin(ctx, create); const PlayerElement = PlayerMixin(MediaElement); - const ProviderMixin = createProviderMixin(ctx, create); - const ContainerMixin = createContainerMixin(ctx); + const ProviderMixin = createProviderMixin(ctx, create); + const ContainerMixin = createContainerMixin(ctx); return { context: ctx, diff --git a/packages/html/src/player/tests/create-player.test-d.ts b/packages/html/src/player/tests/create-player.test-d.ts new file mode 100644 index 00000000..56d66a55 --- /dev/null +++ b/packages/html/src/player/tests/create-player.test-d.ts @@ -0,0 +1,84 @@ +import type { AudioPlayerStore, PlayerStore, PlayerTarget, VideoPlayerStore } from '@videojs/core/dom'; +import { definePlayerFeature, features } from '@videojs/core/dom'; +import type { Slice } from '@videojs/store'; +import { assertType, describe, it } from 'vitest'; + +import { type CreatePlayerResult, createPlayer } from '../create-player'; + +describe('createPlayer', () => { + it('resolves video features to VideoPlayerStore', () => { + const result = createPlayer({ features: features.video }); + + assertType>(result); + }); + + it('resolves audio features to AudioPlayerStore', () => { + const result = createPlayer({ features: features.audio }); + + assertType>(result); + }); + + it('resolves spread video features to VideoPlayerStore', () => { + const result = createPlayer({ features: [...features.video] }); + + assertType>(result); + }); + + it('resolves custom features to generic PlayerStore', () => { + interface CustomState { + custom: boolean; + } + + const customFeature = definePlayerFeature({ + state: (): CustomState => ({ custom: true }), + }); + + const result = createPlayer({ features: [customFeature] }); + + assertType]>>>(result); + }); + + it('resolves extended video features to generic PlayerStore', () => { + interface AnalyticsState { + events: string[]; + } + + const analyticsFeature = definePlayerFeature({ + state: (): AnalyticsState => ({ events: [] }), + }); + + const result = createPlayer({ + features: [...features.video, analyticsFeature], + }); + + // Extended features fall through to the generic overload + assertType>>(result); + + // The store has both video and analytics state + const store = result.create(); + + assertType(store.paused); + assertType(store.volume); + assertType(store.events); + }); + + it('resolves extended audio features to generic PlayerStore', () => { + interface AnalyticsState { + events: string[]; + } + + const analyticsFeature = definePlayerFeature({ + state: (): AnalyticsState => ({ events: [] }), + }); + + const result = createPlayer({ + features: [...features.audio, analyticsFeature], + }); + + const store = result.create(); + + assertType(store.paused); + assertType(store.volume); + assertType(store.events); + }); +}); diff --git a/packages/html/src/player/tests/create-player.test.ts b/packages/html/src/player/tests/create-player.test.ts index 2acb64fb..2396ff8e 100644 --- a/packages/html/src/player/tests/create-player.test.ts +++ b/packages/html/src/player/tests/create-player.test.ts @@ -5,7 +5,7 @@ import { createPlayer } from '../create-player'; describe('createPlayer', () => { it('returns expected exports', () => { - const result = createPlayer({ features: [...features.video] }); + const result = createPlayer({ features: features.video }); expect(result.context).toBeDefined(); expect(result.create).toBeInstanceOf(Function); @@ -17,7 +17,7 @@ describe('createPlayer', () => { }); it('create() returns a store instance', () => { - const { create } = createPlayer({ features: [...features.video] }); + const { create } = createPlayer({ features: features.video }); const store = create(); expect(store.attach).toBeInstanceOf(Function); @@ -26,7 +26,7 @@ describe('createPlayer', () => { }); it('PlayerElement is a valid custom element class', () => { - const { PlayerElement } = createPlayer({ features: [...features.video] }); + const { PlayerElement } = createPlayer({ features: features.video }); expect(typeof PlayerElement).toBe('function'); expect(PlayerElement.prototype).toBeDefined(); diff --git a/packages/react/src/player/create-player.tsx b/packages/react/src/player/create-player.tsx index 30b7de6d..b69a68ce 100644 --- a/packages/react/src/player/create-player.tsx +++ b/packages/react/src/player/create-player.tsx @@ -1,7 +1,17 @@ 'use client'; -import type { AnyPlayerFeature, Media, PlayerStore, PlayerTarget } from '@videojs/core/dom'; -import type { UnionSliceState } from '@videojs/store'; +import type { + AnyPlayerFeature, + AnyPlayerStore, + AudioFeatures, + AudioPlayerStore, + Media, + PlayerStore, + PlayerTarget, + VideoFeatures, + VideoPlayerStore, +} from '@videojs/core/dom'; +import type { InferStoreState } from '@videojs/store'; import { combine, createStore } from '@videojs/store'; import { useStore } from '@videojs/store/react'; import type { FC, ReactNode } from 'react'; @@ -18,24 +28,27 @@ export interface ProviderProps { children: ReactNode; } -export interface CreatePlayerResult { +export interface CreatePlayerResult { Provider: FC; Container: typeof Container; - usePlayer: UsePlayerHook; + usePlayer: UsePlayerHook; useMedia: () => Media | null; } -type UsePlayerHook = { - (): PlayerStore; - (selector: (state: UnionSliceState) => R): R; +export type UsePlayerHook = { + (): Store; + (selector: (state: InferStoreState) => R): R; }; +export function createPlayer(config: CreatePlayerConfig): CreatePlayerResult; + +export function createPlayer(config: CreatePlayerConfig): CreatePlayerResult; + export function createPlayer( config: CreatePlayerConfig -): CreatePlayerResult { - type Store = PlayerStore; - type State = UnionSliceState; +): CreatePlayerResult>; +export function createPlayer(config: CreatePlayerConfig): CreatePlayerResult { function Provider({ children }: ProviderProps): ReactNode { const [store] = useState(() => createStore()(combine(...config.features))); const [media, setMedia] = useState(null); @@ -49,9 +62,7 @@ export function createPlayer( Provider.displayName = `${config.displayName}.Provider`; } - function usePlayer(): Store; - function usePlayer(selector: (state: State) => R): R; - function usePlayer(selector?: (state: State) => R): Store | R { + function usePlayer(selector?: (state: object) => R): AnyPlayerStore | R { const { store } = usePlayerContext(); return useStore(store, selector as any); } @@ -59,7 +70,7 @@ export function createPlayer( return { Provider, Container, - usePlayer: usePlayer as UsePlayerHook, + usePlayer, useMedia, }; } diff --git a/packages/react/src/player/tests/create-player.test-d.tsx b/packages/react/src/player/tests/create-player.test-d.tsx new file mode 100644 index 00000000..280373cb --- /dev/null +++ b/packages/react/src/player/tests/create-player.test-d.tsx @@ -0,0 +1,73 @@ +import type { AudioPlayerStore, PlayerStore, PlayerTarget, VideoPlayerStore } from '@videojs/core/dom'; +import { definePlayerFeature, features } from '@videojs/core/dom'; +import type { Slice } from '@videojs/store'; +import { assertType, describe, it } from 'vitest'; + +import { type CreatePlayerResult, createPlayer } from '../create-player'; + +describe('createPlayer', () => { + it('resolves video features to VideoPlayerStore', () => { + const result = createPlayer({ features: features.video }); + + assertType>(result); + }); + + it('resolves audio features to AudioPlayerStore', () => { + const result = createPlayer({ features: features.audio }); + + assertType>(result); + }); + + it('resolves spread video features to VideoPlayerStore', () => { + const result = createPlayer({ features: [...features.video] }); + + assertType>(result); + }); + + it('resolves custom features to generic PlayerStore', () => { + interface CustomState { + custom: boolean; + } + + const customFeature = definePlayerFeature({ + state: (): CustomState => ({ custom: true }), + }); + + const result = createPlayer({ features: [customFeature] }); + + assertType]>>>(result); + }); + + it('resolves extended video features to generic PlayerStore', () => { + interface AnalyticsState { + events: string[]; + } + + const analyticsFeature = definePlayerFeature({ + state: (): AnalyticsState => ({ events: [] }), + }); + + const result = createPlayer({ + features: [...features.video, analyticsFeature], + }); + + // Extended features fall through to the generic overload + assertType>>(result); + }); + + it('resolves extended audio features to generic PlayerStore', () => { + interface AnalyticsState { + events: string[]; + } + + const analyticsFeature = definePlayerFeature({ + state: (): AnalyticsState => ({ events: [] }), + }); + + const result = createPlayer({ + features: [...features.audio, analyticsFeature], + }); + + assertType>>(result); + }); +});