refactor(media)!: extract media package from core (#1879)

This commit is contained in:
rahim
2026-07-28 21:14:50 -07:00
committed by GitHub
parent fa54f75d38
commit 75dcc6675b
346 changed files with 1570 additions and 1072 deletions
@@ -13,7 +13,7 @@
* - Feature exports: const matching *Feature (singular, not *Features)
* - State type: explicit return type annotation on the state() arrow function
* - Silent features: state() returns an empty object
* - State interfaces: exported from packages/core/src/core/media/state.ts
* - State interfaces: exported from packages/media/src/core/state.ts
*/
import * as fs from 'node:fs';
import * as path from 'node:path';
@@ -190,7 +190,7 @@ function extractInterfaceMembers(
export function generateFeatureReferences(monorepoRoot: string): FeatureResult[] {
const featuresDir = path.join(monorepoRoot, 'packages/core/src/dom/store/features');
const stateFilePath = path.join(monorepoRoot, 'packages/core/src/core/media/state.ts');
const stateFilePath = path.join(monorepoRoot, 'packages/media/src/core/state.ts');
if (!fs.existsSync(featuresDir) || !fs.existsSync(stateFilePath)) return [];
@@ -6,11 +6,12 @@
*
* Convention:
* - Define files: packages/html/src/define/media/*.ts with inline class + static tagName
* - Media element classes: packages/html/src/media/{name}/index.ts
* - Media element classes: packages/html/src/media/{name}/media.ts
* composed as MediaAttachMixin(CustomMediaElement('video'|'audio', Host))
* - Host classes: packages/core/src/dom/media/{name}/index.ts extending
* - React media components: packages/react/src/media/{name}/media.tsx
* - Host classes: packages/media/src/dom/{name}/media.ts extending
* HTMLVideoElementHost or HTMLAudioElementHost with getter/setter pairs
* - Shared data: packages/core/src/dom/media/custom-media-element/index.ts
* - Shared data: packages/media/src/dom/custom-media-element/custom-media-element.ts
* exports CustomMediaElement factory (with static properties), VideoCSSVars,
* AudioCSSVars
*
@@ -66,7 +67,7 @@ interface StaticMediaProperty {
/**
* Resolve an import specifier to an absolute file path using TypeScript's
* module resolution. Handles both relative paths and workspace package
* imports (e.g., @videojs/core/dom/media/hls) via the project's tsconfig.
* imports (e.g., @videojs/media/dom/hls) via the project's tsconfig.
*
* Workspace imports go through `package.json#exports` and resolve to the
* built `dist/dev/*.d.ts` files, where the TypeScript compiler has collapsed
@@ -86,15 +87,31 @@ function resolveModuleToFile(
}
function mapDistToSource(resolvedPath: string): string {
if (!resolvedPath.endsWith('.d.ts')) return resolvedPath;
const match = resolvedPath.match(/^(.+\/packages\/[^/]+)\/dist\/dev\/(.+)\.d\.ts$/);
if (!match) return resolvedPath;
const [, pkgRoot, rest] = match;
const candidates = [`${pkgRoot}/src/${rest}.ts`, `${pkgRoot}/src/${rest}/index.ts`];
for (const candidate of candidates) {
if (fs.existsSync(candidate)) return candidate;
let sourcePath = resolvedPath;
if (resolvedPath.endsWith('.d.ts')) {
const match = resolvedPath.match(/^(.+\/packages\/[^/]+)\/dist\/dev\/(.+)\.d\.ts$/);
if (!match) return resolvedPath;
const [, pkgRoot, rest] = match;
const candidates = [
`${pkgRoot}/src/${rest}.ts`,
`${pkgRoot}/src/${rest}.tsx`,
`${pkgRoot}/src/${rest}/index.ts`,
`${pkgRoot}/src/${rest}/index.tsx`,
];
const candidate = candidates.find((path) => fs.existsSync(path));
if (!candidate) return resolvedPath;
sourcePath = candidate;
}
return resolvedPath;
if (sourcePath.endsWith('/index.ts') || sourcePath.endsWith('/index.tsx')) {
const mediaPath = ['media.ts', 'media.tsx']
.map((file) => path.join(path.dirname(sourcePath), file))
.find((file) => fs.existsSync(file));
if (mediaPath) return mediaPath;
}
return sourcePath;
}
// ─── Discovery ───────────────────────────────────────────────────────
@@ -1044,7 +1061,7 @@ function extractReactReference(
propertyDefinitions: Record<string, HostPropertyDef>
): ReactMediaReference | undefined {
const mediaDirectory = path.basename(path.dirname(source.mediaFilePath));
const reactFilePath = path.join(monorepoRoot, 'packages/react/src/media', mediaDirectory, 'index.tsx');
const reactFilePath = path.join(monorepoRoot, 'packages/react/src/media', mediaDirectory, 'media.tsx');
if (!fs.existsSync(reactFilePath)) return undefined;
const content = fs.readFileSync(reactFilePath, 'utf-8');
@@ -1518,14 +1535,17 @@ export function generateMediaElementReferences(monorepoRoot: string): MediaEleme
const sources = discoverMediaElements(monorepoRoot, compilerOptions);
if (sources.length === 0) return [];
const customMediaPath = path.join(monorepoRoot, 'packages/core/src/dom/media/custom-media-element/index.ts');
const customMediaPath = path.join(
monorepoRoot,
'packages/media/src/dom/custom-media-element/custom-media-element.ts'
);
if (!fs.existsSync(customMediaPath)) return [];
// Read shared data
const staticProperties = extractStaticProperties(customMediaPath);
// Extract events from capability contract types
const mediaTypesPath = path.join(monorepoRoot, 'packages/core/src/core/media/types.ts');
const mediaTypesPath = path.join(monorepoRoot, 'packages/media/src/core/types.ts');
const videoEvents = fs.existsSync(mediaTypesPath) ? extractEventsFromTypes(mediaTypesPath, 'VideoEvents') : [];
const audioEvents = fs.existsSync(mediaTypesPath) ? extractEventsFromTypes(mediaTypesPath, 'AudioEvents') : [];
@@ -1546,9 +1566,9 @@ export function generateMediaElementReferences(monorepoRoot: string): MediaEleme
// from the shared base host classes — extracted ONCE per media type (mirroring
// how events come from VideoEvents/AudioEvents), not per element. Video adds
// the video-host methods; audio adds the audio-host methods.
const mediaHostPath = path.join(monorepoRoot, 'packages/core/src/dom/media/media-host.ts');
const videoHostPath = path.join(monorepoRoot, 'packages/core/src/dom/media/video-host.ts');
const audioHostPath = path.join(monorepoRoot, 'packages/core/src/dom/media/audio-host.ts');
const mediaHostPath = path.join(monorepoRoot, 'packages/media/src/dom/media-host/media-host.ts');
const videoHostPath = path.join(monorepoRoot, 'packages/media/src/dom/video-host/video-host.ts');
const audioHostPath = path.join(monorepoRoot, 'packages/media/src/dom/audio-host/audio-host.ts');
const baseMethods = extractPublicMethodNames(mediaHostPath, 'HTMLMediaElementHost');
const videoMethods = mergeMethodNames(baseMethods, extractPublicMethodNames(videoHostPath, 'HTMLVideoElementHost'));
const audioMethods = mergeMethodNames(baseMethods, extractPublicMethodNames(audioHostPath, 'HTMLAudioElementHost'));
@@ -69,7 +69,7 @@
* media element export, tailwind skin exclusion.
* audio/ Exercises: single skin, different media element.
*
* Media elements (packages/html/src/define/media/ + packages/core/src/dom/media/):
* Media elements (packages/html/src/define/media/ + packages/media/src/dom/):
* simple-video Simple media element. Exercises: discovery via static
* tagName in define/media/*.ts, minimal host (src rw,
* engine readonly), shared attributes/events/CSS vars
@@ -1600,7 +1600,7 @@ describe('Media element pipeline (end-to-end)', () => {
// ─────────────────────────────────────────────────────────────────
//
// Events are derived from the capability contract types in
// packages/core/src/core/media/types.ts, not hardcoded.
// packages/media/src/core/types.ts, not hardcoded.
// VideoEvents includes TextTrackListEvents; AudioEvents does not.
describe('Event extraction from capability contracts', () => {
@@ -4,7 +4,7 @@
* Exercises: simple boolean state properties, void and Promise<void> action methods,
* JSDoc description extraction from the state interface.
*/
import type { MediaPlaybackState } from '../../../core/media/state';
import type { MediaPlaybackState } from '../../../../../media/src/core/state';
import { definePlayerFeature } from '../../feature';
export const playbackFeature = definePlayerFeature({
@@ -4,7 +4,7 @@
* Exercises: numeric state property, type alias (MediaFeatureAvailability),
* methods with parameters and return values, boolean state property.
*/
import type { MediaVolumeState } from '../../../core/media/state';
import type { MediaVolumeState } from '../../../../../media/src/core/state';
import { definePlayerFeature } from '../../feature';
export const volumeFeature = definePlayerFeature({
@@ -1,12 +1 @@
/**
* Mock background video mirrors the real BackgroundVideo.
*
* Exercises: exclusion of elements that use MediaAttachMixin(HTMLElement)
* without MediaPropsMixin. The builder's parseMixinChain returns null
* because there is no MediaPropsMixin call in the extends chain.
*/
function MediaAttachMixin(base: any) {
return base;
}
export class BackgroundVideo extends MediaAttachMixin(Object) {}
export * from './media';
@@ -0,0 +1,12 @@
/**
* Mock background video mirrors the real BackgroundVideo.
*
* Exercises: exclusion of elements that use MediaAttachMixin(HTMLElement)
* without MediaPropsMixin. The builder's parseMixinChain returns null
* because there is no MediaPropsMixin call in the extends chain.
*/
function MediaAttachMixin(base: any) {
return base;
}
export class BackgroundVideo extends MediaAttachMixin(Object) {}
@@ -1,16 +1 @@
/**
* Mock complex media element mirrors HlsVideo.
*
* Exercises: standard composition with CustomMediaElement factory
* and a complex host that has JSDoc descriptions on its getter/setters.
*/
import { ComplexHost } from '../../../../core/src/dom/media/complex';
import { CustomMediaElement } from '../../../../core/src/dom/media/custom-media-element';
// Stub — the builder parses the AST, it doesn't run the code.
function MediaAttachMixin(base: any) {
return base;
}
export class ComplexVideo extends MediaAttachMixin(CustomMediaElement('video', ComplexHost)) {}
export * from './media';
@@ -0,0 +1,16 @@
/**
* Mock complex media element mirrors HlsVideo.
*
* Exercises: standard composition with CustomMediaElement factory
* and a complex host that has JSDoc descriptions on its getter/setters.
*/
import { ComplexHost } from '../../../../media/src/dom/complex';
import { CustomMediaElement } from '../../../../media/src/dom/custom-media-element';
// Stub — the builder parses the AST, it doesn't run the code.
function MediaAttachMixin(base: any) {
return base;
}
export class ComplexVideo extends MediaAttachMixin(CustomMediaElement('video', ComplexHost)) {}
@@ -1,10 +1 @@
import { CustomMediaElement } from '../../../../core/src/dom/media/custom-media-element';
import { EmbedHost } from '../../../../core/src/dom/media/embed';
function MediaAttachMixin(base: any) {
return base;
}
class EmbedCustomMediaElement extends CustomMediaElement('iframe', EmbedHost) {}
export class EmbedVideo extends MediaAttachMixin(EmbedCustomMediaElement) {}
export * from './media';
@@ -0,0 +1,10 @@
import { CustomMediaElement } from '../../../../media/src/dom/custom-media-element';
import { EmbedHost } from '../../../../media/src/dom/embed';
function MediaAttachMixin(base: any) {
return base;
}
class EmbedCustomMediaElement extends CustomMediaElement('iframe', EmbedHost) {}
export class EmbedVideo extends MediaAttachMixin(EmbedCustomMediaElement) {}
@@ -1,16 +1 @@
/**
* Mock extending media element mirrors MuxVideo.
*
* Exercises: media element with a host that inherits from another host,
* plus a hoisted-const base with an `as` cast in the extends clause.
*/
import { CustomMediaElement } from '../../../../core/src/dom/media/custom-media-element';
import { ExtendingHost } from '../../../../core/src/dom/media/extending';
function MediaAttachMixin(base: any) {
return base;
}
const ExtendingVideoBase = MediaAttachMixin(CustomMediaElement('video', ExtendingHost));
export class ExtendingVideo extends (ExtendingVideoBase as typeof ExtendingVideoBase) {}
export * from './media';
@@ -0,0 +1,16 @@
/**
* Mock extending media element mirrors MuxVideo.
*
* Exercises: media element with a host that inherits from another host,
* plus a hoisted-const base with an `as` cast in the extends clause.
*/
import { CustomMediaElement } from '../../../../media/src/dom/custom-media-element';
import { ExtendingHost } from '../../../../media/src/dom/extending';
function MediaAttachMixin(base: any) {
return base;
}
const ExtendingVideoBase = MediaAttachMixin(CustomMediaElement('video', ExtendingHost));
export class ExtendingVideo extends (ExtendingVideoBase as typeof ExtendingVideoBase) {}
@@ -1,14 +1 @@
/**
* Mock mixin-chain media element mirrors MuxVideo / NativeHlsVideo.
*
* Exercises: standard composition where the host is a mixin chain.
*/
import { CustomMediaElement } from '../../../../core/src/dom/media/custom-media-element';
import { MixinHost } from '../../../../core/src/dom/media/mixin';
// Stub — the builder parses the AST, it doesn't run the code.
function MediaAttachMixin(base: any) {
return base;
}
export class MixinVideo extends MediaAttachMixin(CustomMediaElement('video', MixinHost)) {}
export * from './media';
@@ -0,0 +1,14 @@
/**
* Mock mixin-chain media element mirrors MuxVideo / NativeHlsVideo.
*
* Exercises: standard composition where the host is a mixin chain.
*/
import { CustomMediaElement } from '../../../../media/src/dom/custom-media-element';
import { MixinHost } from '../../../../media/src/dom/mixin';
// Stub — the builder parses the AST, it doesn't run the code.
function MediaAttachMixin(base: any) {
return base;
}
export class MixinVideo extends MediaAttachMixin(CustomMediaElement('video', MixinHost)) {}
@@ -1,16 +1 @@
/**
* Mock simple media element mirrors DashVideo.
*
* Exercises: standard composition with CustomMediaElement factory.
* The builder follows this import chain to discover the host class
* and resolve its properties.
*/
import { CustomMediaElement } from '../../../../core/src/dom/media/custom-media-element';
import { SimpleHost } from '../../../../core/src/dom/media/simple';
// Stub — the builder parses the AST, it doesn't run the code.
function MediaAttachMixin(base: any) {
return base;
}
export class SimpleVideo extends MediaAttachMixin(CustomMediaElement('video', SimpleHost)) {}
export * from './media';
@@ -0,0 +1,16 @@
/**
* Mock simple media element mirrors DashVideo.
*
* Exercises: standard composition with CustomMediaElement factory.
* The builder follows this import chain to discover the host class
* and resolve its properties.
*/
import { CustomMediaElement } from '../../../../media/src/dom/custom-media-element';
import { SimpleHost } from '../../../../media/src/dom/simple';
// Stub — the builder parses the AST, it doesn't run the code.
function MediaAttachMixin(base: any) {
return base;
}
export class SimpleVideo extends MediaAttachMixin(CustomMediaElement('video', SimpleHost)) {}
@@ -1,15 +1 @@
/**
* Mock audio-only media element mirrors SimpleHlsAudioOnly.
*
* Exercises: audio media type ('audio' tag argument) with a cross-package
* mixin host.
*/
import { CustomMediaElement } from '../../../../core/src/dom/media/custom-media-element';
import { SpfAudioHost } from '../../../../core/src/dom/media/spf-audio';
// Stub — the builder parses the AST, it doesn't run the code.
function MediaAttachMixin(base: any) {
return base;
}
export class SpfAudio extends MediaAttachMixin(CustomMediaElement('audio', SpfAudioHost)) {}
export * from './media';
@@ -0,0 +1,15 @@
/**
* Mock audio-only media element mirrors SimpleHlsAudioOnly.
*
* Exercises: audio media type ('audio' tag argument) with a cross-package
* mixin host.
*/
import { CustomMediaElement } from '../../../../media/src/dom/custom-media-element';
import { SpfAudioHost } from '../../../../media/src/dom/spf-audio';
// Stub — the builder parses the AST, it doesn't run the code.
function MediaAttachMixin(base: any) {
return base;
}
export class SpfAudio extends MediaAttachMixin(CustomMediaElement('audio', SpfAudioHost)) {}
@@ -1,5 +1,5 @@
/**
* Mock media contract types mirrors packages/core/src/core/media/types.ts.
* Mock media contract types mirrors packages/media/src/core/types.ts.
*
* Exercises: event extraction from capability event interfaces.
* VideoEvents extends all capability events (including TextTrackListEvents).
@@ -4,6 +4,6 @@
* Adds no methods of its own: audio elements get only the shared media-host
* methods.
*/
import { HTMLMediaElementHost } from './media-host';
import { HTMLMediaElementHost } from '../media-host';
export class HTMLAudioElementHost extends HTMLMediaElementHost {}
@@ -0,0 +1 @@
export * from './audio-host';
@@ -0,0 +1 @@
export * from './media';
@@ -7,7 +7,7 @@
* values declared in a co-located `*DefaultProps` export (mirrors
* hlsMediaDefaultProps) including a const-object member reference.
*/
import { MediaStreamTypes } from '../../../core/media/types';
import { MediaStreamTypes } from '../../core/types';
import { HTMLVideoElementHost } from '../simple';
export const complexMediaDefaultProps = {
@@ -0,0 +1 @@
export * from './custom-media-element';
@@ -0,0 +1 @@
export * from './media';
@@ -0,0 +1 @@
export * from './media';
@@ -0,0 +1 @@
export * from './media-host';
@@ -0,0 +1 @@
export * from './media';
@@ -0,0 +1 @@
export * from './media';
@@ -0,0 +1 @@
export * from './media';
@@ -5,7 +5,7 @@
* (spf), reached through that package's barrel file, composed onto the
* audio host base.
*/
import { SpfAudioOnlyMediaMixin } from '../../../../../spf/src/hls';
import { SpfAudioOnlyMediaMixin } from '../../../../spf/src/hls';
import { HTMLAudioElementHost } from '../simple';
export class SpfAudioHost extends SpfAudioOnlyMediaMixin(HTMLAudioElementHost) {}
@@ -0,0 +1 @@
export * from './video-host';
@@ -6,7 +6,7 @@
* video-only native-property extraction (videoWidth) and a non-native helper
* (isFullscreen) that must be filtered out of nativeProperties.
*/
import { HTMLMediaElementHost } from './media-host';
import { HTMLMediaElementHost } from '../media-host';
export class HTMLVideoElementHost extends HTMLMediaElementHost {
requestFullscreen(): Promise<void> {
@@ -0,0 +1 @@
export * from './media';
@@ -5,7 +5,7 @@
* native React video props, a forwarded native-element ref, and a defaults
* object passed to useSyncProps for Video.js-specific props.
*/
import { complexMediaDefaultProps } from '../../../../core/src/dom/media/complex';
import { complexMediaDefaultProps } from '../../../../media/src/dom/complex';
interface VideoHTMLAttributes<Element> {
element?: Element;
@@ -0,0 +1 @@
export * from './media';
@@ -1,4 +1,4 @@
import { embedMediaDefaultProps } from '../../../../core/src/dom/media/embed';
import { embedMediaDefaultProps } from '../../../../media/src/dom/embed';
interface EmbedVideoProps extends Partial<typeof embedMediaDefaultProps> {}