refactor(packages): dry up core, html, and react UI architecture (#699)

This commit is contained in:
rahim
2026-03-03 23:27:53 -08:00
committed by GitHub
parent 8bdd4ce7fa
commit 1edeadefed
110 changed files with 1349 additions and 1359 deletions
+16 -8
View File
@@ -1,6 +1,7 @@
import { pick } from '@videojs/utils/object';
import { AbortControllerRegistry } from './abort-controller-registry';
import { throwNoTargetError } from './errors';
import type { Selector } from './shallow-equal';
import type { AnySlice, InferSliceState, StateContext } from './slice';
const stateContext: StateContext<unknown> = {
@@ -18,20 +19,27 @@ const stateContext: StateContext<unknown> = {
* ```ts
* const selectPlayback = createSelector(playbackSlice);
* selectPlayback(store.state); // { paused, play, pause, ... } | undefined
* selectPlayback.displayName; // 'playback' (from slice name)
* ```
*
* @param slice - The feature slice to create a selector for.
* @param slice - The slice to create a selector for.
*/
export function createSelector<S extends AnySlice>(slice: S): (state: object) => InferSliceState<S> | undefined {
export function createSelector<S extends AnySlice>(slice: S): Selector<object, InferSliceState<S> | undefined> {
const initialState = slice.state(stateContext);
const keys = Object.keys(initialState as object);
const firstKey = keys[0];
if (!firstKey) return () => undefined;
return (state) => {
// WARN: Could be the source of a bug if two slices have overlapping state keys
if (!(firstKey in state)) return undefined;
return pick(state as Record<string, unknown>, keys) as InferSliceState<S>;
};
if (!firstKey) {
return Object.assign(() => undefined, { displayName: slice.name });
}
return Object.assign(
(state: object) => {
// WARN: Could be the source of a bug if two slices have overlapping state keys
if (!(firstKey in state)) return undefined;
return pick(state as Record<string, unknown>, keys) as InferSliceState<S>;
},
{ displayName: slice.name }
);
}
+4 -1
View File
@@ -1,4 +1,7 @@
export type Selector<State, Result> = (state: State) => Result;
export interface Selector<State, Result> {
(state: State): Result;
displayName?: string | undefined;
}
export type Comparator<T> = (a: T, b: T) => boolean;
+2
View File
@@ -47,6 +47,8 @@ export interface StateContext<Target> {
// ----------------------------------------
export interface SliceConfig<Target, State> {
/** Debug label. Used as `displayName` on selectors created from this slice. */
name?: string;
state: (ctx: StateContext<Target>) => State;
attach?: (ctx: AttachContext<Target, State>) => void;
}
@@ -8,6 +8,7 @@ interface MockMedia {
describe('createSelector', () => {
const volumeSlice = defineSlice<MockMedia>()({
name: 'volume',
state: ({ target }) => ({
volume: 1,
muted: false,
@@ -19,6 +20,7 @@ describe('createSelector', () => {
});
const playbackSlice = defineSlice<MockMedia>()({
name: 'playback',
state: () => ({
paused: true,
ended: false,
@@ -86,4 +88,30 @@ describe('createSelector', () => {
// But structurally equal (for shallowEqual comparison)
expect(selected1).toEqual(selected2);
});
it('exposes displayName from slice name', () => {
const selectVolume = createSelector(volumeSlice);
expect(selectVolume.displayName).toBe('volume');
});
it('omits displayName when slice has no name', () => {
const unnamedSlice = defineSlice<MockMedia>()({
state: () => ({ paused: true }),
});
const selector = createSelector(unnamedSlice);
expect(selector.displayName).toBeUndefined();
});
it('returns undefined for empty-state slice', () => {
const emptySlice = defineSlice<MockMedia>()({
name: 'empty',
state: () => ({}),
});
const selector = createSelector(emptySlice);
expect(selector({})).toBeUndefined();
expect(selector.displayName).toBe('empty');
});
});