mirror of
https://github.com/zoriya/v10.git
synced 2026-08-05 13:48:14 +00:00
chore(site): rewrite media element builder for MediaHost architecture (#1334)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
b731cba6ba
commit
d8cd59e0fd
@@ -2,29 +2,40 @@
|
||||
* Media element reference extraction.
|
||||
*
|
||||
* Discovers media elements from packages/html/src/define/media/*.ts and extracts
|
||||
* delegate properties, shared attributes/events/CSS vars, and slots.
|
||||
* host properties, shared attributes/events/CSS vars, and slots.
|
||||
*
|
||||
* Convention:
|
||||
* - Define files: packages/html/src/define/media/*.ts with inline class + static tagName
|
||||
* - Media element classes: packages/html/src/media/{name}/index.ts
|
||||
* composed as MediaPropsMixin(MediaAttachMixin(CustomMedia), Delegate)
|
||||
* - Delegate classes: packages/core/src/dom/media/{name}/index.ts with getter/setter pairs.
|
||||
* CustomMedia classes use inheritance mixins (e.g., HlsMediaMixin(CustomVideoElement)).
|
||||
* composed as MediaAttachMixin(CustomMediaElement('video'|'audio', Host))
|
||||
* - Host classes: packages/core/src/dom/media/{name}/index.ts extending
|
||||
* HTMLVideoElementHost or HTMLAudioElementHost with getter/setter pairs
|
||||
* - Shared data: packages/core/src/dom/media/custom-media-element/index.ts
|
||||
* exports Attributes, Events, VideoCSSVars, AudioCSSVars, and template functions
|
||||
* - Slots: parsed from getVideoTemplateHTML / getAudioTemplateHTML in custom-media-element
|
||||
* exports CustomMediaElement factory (with static properties), VideoCSSVars,
|
||||
* AudioCSSVars, and template functions
|
||||
* - Slots: parsed from getVideoTemplateHTML / getCommonTemplateHTML in custom-media-element
|
||||
*
|
||||
* Exclusions (elements discovered but intentionally skipped):
|
||||
* - container.ts: re-exports a class, doesn't declare one inline → no static tagName found
|
||||
* - background-video.ts: uses MediaAttachMixin(HTMLElement) without MediaPropsMixin →
|
||||
* parseMixinChain returns null. Its API reference is manually maintained in MDX.
|
||||
* - background-video.ts: uses MediaAttachMixin(HTMLElement) without CustomMediaElement →
|
||||
* parseCustomMediaElementCall returns null. Its API reference is manually maintained in MDX.
|
||||
*/
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import * as ts from 'typescript';
|
||||
import * as tae from 'typescript-api-extractor';
|
||||
import { extractCSSVars } from './css-vars-handler.js';
|
||||
import type { DelegatePropertyDef, MediaElementReference, MediaElementResult } from './pipeline.js';
|
||||
import type { HostPropertyDef, MediaElementReference, MediaElementResult } from './pipeline.js';
|
||||
|
||||
// ─── Constants ──────────────────────────────────────────────────────
|
||||
|
||||
/** Classes that mark the end of the host prototype chain for property extraction. */
|
||||
const HOST_BASE_CLASSES = new Set([
|
||||
'HTMLMediaElementHost',
|
||||
'HTMLVideoElementHost',
|
||||
'HTMLAudioElementHost',
|
||||
'EventTarget',
|
||||
]);
|
||||
|
||||
// ─── Types ───────────────────────────────────────────────────────────
|
||||
|
||||
@@ -33,9 +44,9 @@ interface MediaElementSource {
|
||||
className: string;
|
||||
tagName: string;
|
||||
mediaFilePath: string;
|
||||
delegateFilePath: string;
|
||||
delegateClassName: string;
|
||||
customMediaClassName: string;
|
||||
hostFilePath: string;
|
||||
hostClassName: string;
|
||||
mediaType: 'video' | 'audio';
|
||||
}
|
||||
|
||||
// ─── Module Resolution ───────────────────────────────────────────────
|
||||
@@ -80,7 +91,7 @@ function discoverMediaElements(monorepoRoot: string, compilerOptions: ts.Compile
|
||||
/**
|
||||
* Parse a define/media file to extract class name, tagName, and import chain.
|
||||
* Returns null if the file doesn't declare an inline class with static tagName
|
||||
* (container.ts) or if the class doesn't use MediaPropsMixin (background-video.ts).
|
||||
* (container.ts) or if the class doesn't use CustomMediaElement (background-video.ts).
|
||||
*/
|
||||
function parseDefineFile(
|
||||
sourceFile: ts.SourceFile,
|
||||
@@ -140,15 +151,15 @@ function parseDefineFile(
|
||||
const mediaFilePath = resolveModuleToFile(filePath, baseImportPath, compilerOptions);
|
||||
if (!mediaFilePath) return null;
|
||||
|
||||
// Parse the media element file to find the delegate class
|
||||
// Parse the media element file to find the CustomMediaElement(tag, Host) call
|
||||
const mediaContent = fs.readFileSync(mediaFilePath, 'utf-8');
|
||||
const mediaSourceFile = ts.createSourceFile(mediaFilePath, mediaContent, ts.ScriptTarget.Latest, true);
|
||||
|
||||
const delegateInfo = parseMixinChain(mediaSourceFile, baseClassName);
|
||||
if (!delegateInfo) return null;
|
||||
const hostInfo = parseCustomMediaElementCall(mediaSourceFile, baseClassName);
|
||||
if (!hostInfo) return null;
|
||||
|
||||
// Resolve delegate import path
|
||||
let delegateImportPath: string | undefined;
|
||||
// Resolve host class import path
|
||||
let hostImportPath: string | undefined;
|
||||
ts.forEachChild(mediaSourceFile, (node) => {
|
||||
if (!ts.isImportDeclaration(node)) return;
|
||||
if (!ts.isStringLiteral(node.moduleSpecifier)) return;
|
||||
@@ -156,26 +167,26 @@ function parseDefineFile(
|
||||
if (!importClause?.namedBindings || !ts.isNamedImports(importClause.namedBindings)) return;
|
||||
|
||||
for (const specifier of importClause.namedBindings.elements) {
|
||||
if (specifier.name.text === delegateInfo.delegateClassName) {
|
||||
delegateImportPath = node.moduleSpecifier.text;
|
||||
if (specifier.name.text === hostInfo.hostClassName) {
|
||||
hostImportPath = node.moduleSpecifier.text;
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!delegateImportPath) return null;
|
||||
if (!hostImportPath) return null;
|
||||
|
||||
const delegateFilePath = resolveModuleToFile(mediaFilePath, delegateImportPath, compilerOptions);
|
||||
if (!delegateFilePath) return null;
|
||||
const hostFilePath = resolveModuleToFile(mediaFilePath, hostImportPath, compilerOptions);
|
||||
if (!hostFilePath) return null;
|
||||
|
||||
return {
|
||||
defineFilePath: filePath,
|
||||
className: stripElementSuffix(className),
|
||||
tagName,
|
||||
mediaFilePath,
|
||||
delegateFilePath,
|
||||
delegateClassName: delegateInfo.delegateClassName,
|
||||
customMediaClassName: delegateInfo.customMediaClassName,
|
||||
hostFilePath,
|
||||
hostClassName: hostInfo.hostClassName,
|
||||
mediaType: hostInfo.mediaType,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -184,15 +195,15 @@ function stripElementSuffix(name: string): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the media element class to find the MediaPropsMixin(Base, Delegate) call.
|
||||
* Returns null for elements that don't use MediaPropsMixin (e.g., BackgroundVideo).
|
||||
* Parse the media element class to find the CustomMediaElement(tag, Host) call.
|
||||
* Returns null for elements that don't use CustomMediaElement (e.g., BackgroundVideo).
|
||||
*/
|
||||
function parseMixinChain(
|
||||
function parseCustomMediaElementCall(
|
||||
sourceFile: ts.SourceFile,
|
||||
className: string
|
||||
): { delegateClassName: string; customMediaClassName: string } | null {
|
||||
let delegateClassName: string | undefined;
|
||||
let customMediaClassName: string | undefined;
|
||||
): { hostClassName: string; mediaType: 'video' | 'audio' } | null {
|
||||
let hostClassName: string | undefined;
|
||||
let mediaType: 'video' | 'audio' | undefined;
|
||||
|
||||
ts.forEachChild(sourceFile, (node) => {
|
||||
if (!ts.isClassDeclaration(node)) return;
|
||||
@@ -203,60 +214,62 @@ function parseMixinChain(
|
||||
if (!extendsClause || extendsClause.types.length === 0) return;
|
||||
|
||||
const extendsExpr = extendsClause.types[0]!.expression;
|
||||
findMediaPropsMixin(extendsExpr);
|
||||
findCustomMediaElement(extendsExpr);
|
||||
});
|
||||
|
||||
function findMediaPropsMixin(node: ts.Node): void {
|
||||
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'MediaPropsMixin') {
|
||||
function findCustomMediaElement(node: ts.Node): void {
|
||||
if (
|
||||
ts.isCallExpression(node) &&
|
||||
ts.isIdentifier(node.expression) &&
|
||||
node.expression.text === 'CustomMediaElement'
|
||||
) {
|
||||
if (node.arguments.length >= 2) {
|
||||
const delegateArg = node.arguments[1]!;
|
||||
if (ts.isIdentifier(delegateArg)) {
|
||||
delegateClassName = delegateArg.text;
|
||||
// First arg: media type string literal ('video' or 'audio')
|
||||
const tagArg = node.arguments[0]!;
|
||||
if (ts.isStringLiteral(tagArg)) {
|
||||
mediaType = tagArg.text === 'audio' ? 'audio' : 'video';
|
||||
}
|
||||
// Second arg: host class identifier
|
||||
const hostArg = node.arguments[1]!;
|
||||
if (ts.isIdentifier(hostArg)) {
|
||||
hostClassName = hostArg.text;
|
||||
}
|
||||
const baseArg = node.arguments[0]!;
|
||||
customMediaClassName = unwrapMixinBase(baseArg);
|
||||
}
|
||||
return;
|
||||
}
|
||||
ts.forEachChild(node, findMediaPropsMixin);
|
||||
ts.forEachChild(node, findCustomMediaElement);
|
||||
}
|
||||
|
||||
if (!delegateClassName || !customMediaClassName) return null;
|
||||
return { delegateClassName, customMediaClassName };
|
||||
if (!hostClassName || !mediaType) return null;
|
||||
return { hostClassName, mediaType };
|
||||
}
|
||||
|
||||
function unwrapMixinBase(node: ts.Node): string | undefined {
|
||||
if (ts.isIdentifier(node)) return node.text;
|
||||
if (ts.isCallExpression(node) && node.arguments.length > 0) {
|
||||
return unwrapMixinBase(node.arguments[0]!);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// ─── Delegate Property Extraction ────────────────────────────────────
|
||||
// ─── Host Property Extraction ───────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Extract getter/setter pairs from a delegate class and its ancestors,
|
||||
* mirroring what buildAttrPropMap() in media-props-mixin.ts does at runtime.
|
||||
* Extract getter/setter pairs from a host class and its ancestors,
|
||||
* mirroring what CustomMediaElement does at runtime when it walks
|
||||
* the MediaHost prototype chain.
|
||||
*/
|
||||
function extractDelegateProperties(
|
||||
function extractHostProperties(
|
||||
filePath: string,
|
||||
delegateClassName: string,
|
||||
hostClassName: string,
|
||||
compilerOptions: ts.CompilerOptions
|
||||
): Record<string, DelegatePropertyDef> {
|
||||
const properties: Record<string, DelegatePropertyDef> = {};
|
||||
extractClassProperties(filePath, delegateClassName, properties, compilerOptions, new Set());
|
||||
): Record<string, HostPropertyDef> {
|
||||
const properties: Record<string, HostPropertyDef> = {};
|
||||
extractClassProperties(filePath, hostClassName, properties, compilerOptions, new Set());
|
||||
return properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively extract getter/setter pairs from a class and its parent chain.
|
||||
* Child properties override parent properties (checked via the `seen` set).
|
||||
* Stops at host base classes (HTMLMediaElementHost, HTMLVideoElementHost, etc.).
|
||||
*/
|
||||
function extractClassProperties(
|
||||
filePath: string,
|
||||
className: string,
|
||||
properties: Record<string, DelegatePropertyDef>,
|
||||
properties: Record<string, HostPropertyDef>,
|
||||
compilerOptions: ts.CompilerOptions,
|
||||
seen: Set<string>
|
||||
): void {
|
||||
@@ -274,7 +287,7 @@ function extractClassProperties(
|
||||
ts.forEachChild(sourceFile, (node) => {
|
||||
if (!ts.isClassDeclaration(node) || !node.name || node.name.text !== className) return;
|
||||
|
||||
// Check for extends clause (delegate inheritance)
|
||||
// Check for extends clause (host inheritance)
|
||||
if (node.heritageClauses) {
|
||||
const extendsClause = node.heritageClauses.find((h) => h.token === ts.SyntaxKind.ExtendsKeyword);
|
||||
if (extendsClause && extendsClause.types.length > 0) {
|
||||
@@ -308,7 +321,7 @@ function extractClassProperties(
|
||||
});
|
||||
|
||||
// Resolve parent class and extract its properties first (child overrides parent)
|
||||
if (parentClassName && parentClassName !== 'EventTarget') {
|
||||
if (parentClassName && !HOST_BASE_CLASSES.has(parentClassName)) {
|
||||
// Find the import for the parent class
|
||||
ts.forEachChild(sourceFile, (node) => {
|
||||
if (!ts.isImportDeclaration(node)) return;
|
||||
@@ -339,7 +352,7 @@ function extractClassProperties(
|
||||
|
||||
// Apply this class's properties (overrides parent)
|
||||
for (const [name, info] of getters) {
|
||||
const def: DelegatePropertyDef = {
|
||||
const def: HostPropertyDef = {
|
||||
type: info.type,
|
||||
readonly: !setters.has(name),
|
||||
};
|
||||
@@ -371,31 +384,56 @@ function getJSDocDescription(node: ts.Node): string | undefined {
|
||||
|
||||
// ─── Shared Data Extraction ──────────────────────────────────────────
|
||||
|
||||
function extractStringArray(filePath: string, varName: string): string[] {
|
||||
/**
|
||||
* Extract native attribute names from the `static properties` object inside
|
||||
* the CustomMediaElement factory. Each key maps to an attribute name via
|
||||
* `props[key].attribute ?? key.toLowerCase()`.
|
||||
*/
|
||||
function extractStaticProperties(filePath: string): string[] {
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
|
||||
const items: string[] = [];
|
||||
const attributes: string[] = [];
|
||||
|
||||
ts.forEachChild(sourceFile, (node) => {
|
||||
if (!ts.isVariableStatement(node)) return;
|
||||
for (const decl of node.declarationList.declarations) {
|
||||
if (!ts.isIdentifier(decl.name) || decl.name.text !== varName) continue;
|
||||
if (!decl.initializer) continue;
|
||||
function visit(node: ts.Node): void {
|
||||
// Look for: static properties = { ... }
|
||||
if (
|
||||
ts.isPropertyDeclaration(node) &&
|
||||
node.name &&
|
||||
ts.isIdentifier(node.name) &&
|
||||
node.name.text === 'properties' &&
|
||||
node.modifiers?.some((m) => m.kind === ts.SyntaxKind.StaticKeyword) &&
|
||||
node.initializer &&
|
||||
ts.isObjectLiteralExpression(node.initializer)
|
||||
) {
|
||||
for (const prop of node.initializer.properties) {
|
||||
if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) continue;
|
||||
|
||||
let expr = decl.initializer;
|
||||
if (ts.isAsExpression(expr)) expr = expr.expression;
|
||||
const propName = prop.name.text;
|
||||
let attrName = propName.toLowerCase();
|
||||
|
||||
if (ts.isArrayLiteralExpression(expr)) {
|
||||
for (const el of expr.elements) {
|
||||
if (ts.isStringLiteral(el)) {
|
||||
items.push(el.text);
|
||||
// Check for explicit `attribute` override in the property config
|
||||
if (ts.isObjectLiteralExpression(prop.initializer)) {
|
||||
for (const configProp of prop.initializer.properties) {
|
||||
if (
|
||||
ts.isPropertyAssignment(configProp) &&
|
||||
ts.isIdentifier(configProp.name) &&
|
||||
configProp.name.text === 'attribute' &&
|
||||
ts.isStringLiteral(configProp.initializer)
|
||||
) {
|
||||
attrName = configProp.initializer.text;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return items;
|
||||
attributes.push(attrName);
|
||||
}
|
||||
return;
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
}
|
||||
|
||||
visit(sourceFile);
|
||||
return attributes;
|
||||
}
|
||||
|
||||
function extractSlotsFromTemplate(filePath: string, templateFnName: string): string[] {
|
||||
@@ -418,6 +456,47 @@ function extractSlotsFromTemplate(filePath: string, templateFnName: string): str
|
||||
return slots;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract slots from getCommonTemplateHTML — a factory function that returns
|
||||
* a function containing the template string.
|
||||
*/
|
||||
function extractSlotsFromTemplateFactory(filePath: string, factoryFnName: string): string[] {
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
|
||||
const slots: string[] = [];
|
||||
|
||||
function visit(node: ts.Node): void {
|
||||
if (ts.isFunctionDeclaration(node) && node.name?.text === factoryFnName && node.body) {
|
||||
// The factory returns a function — look for a return statement with a function/arrow
|
||||
for (const stmt of node.body.statements) {
|
||||
if (ts.isReturnStatement(stmt) && stmt.expression) {
|
||||
// Could be an arrow function or function expression
|
||||
let innerBody: ts.Block | ts.Expression | undefined;
|
||||
if (ts.isArrowFunction(stmt.expression)) {
|
||||
innerBody = stmt.expression.body;
|
||||
} else if (ts.isFunctionExpression(stmt.expression)) {
|
||||
innerBody = stmt.expression.body;
|
||||
}
|
||||
|
||||
if (innerBody) {
|
||||
const templateText = ts.isBlock(innerBody)
|
||||
? extractTemplateString(innerBody)
|
||||
: getTemplateText(innerBody as ts.Expression);
|
||||
if (templateText) {
|
||||
parseSlots(templateText, slots);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
}
|
||||
|
||||
visit(sourceFile);
|
||||
return slots;
|
||||
}
|
||||
|
||||
function extractTemplateString(block: ts.Block): string | undefined {
|
||||
for (const stmt of block.statements) {
|
||||
if (ts.isReturnStatement(stmt) && stmt.expression) {
|
||||
@@ -453,63 +532,68 @@ function parseSlots(html: string, slots: string[]): void {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Event Extraction ────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Determine whether a CustomMedia base class is video or audio by checking
|
||||
* the extends clause of the class that defines it (e.g., HlsMediaMixin(CustomVideoElement)).
|
||||
* Extract event names from a composite event interface (e.g. VideoEvents, AudioEvents)
|
||||
* by walking its `extends` chain and collecting property keys from each parent interface.
|
||||
*
|
||||
* Convention: capability event interfaces (MediaPlaybackEvents, etc.) are flat
|
||||
* `eventName: EventLike` maps, and VideoEvents/AudioEvents compose them via `extends`.
|
||||
*/
|
||||
function resolveMediaType(
|
||||
mediaFilePath: string,
|
||||
customMediaClassName: string,
|
||||
compilerOptions: ts.CompilerOptions
|
||||
): 'video' | 'audio' {
|
||||
const content = fs.readFileSync(mediaFilePath, 'utf-8');
|
||||
const sourceFile = ts.createSourceFile(mediaFilePath, content, ts.ScriptTarget.Latest, true);
|
||||
function extractEventsFromTypes(filePath: string, interfaceName: string): string[] {
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
|
||||
|
||||
// Build a map of interface name → { extends list, own property keys }
|
||||
const interfaces = new Map<string, { extends: string[]; keys: string[] }>();
|
||||
|
||||
let importSource: string | undefined;
|
||||
ts.forEachChild(sourceFile, (node) => {
|
||||
if (!ts.isImportDeclaration(node)) return;
|
||||
if (!ts.isStringLiteral(node.moduleSpecifier)) return;
|
||||
const importClause = node.importClause;
|
||||
if (!importClause?.namedBindings || !ts.isNamedImports(importClause.namedBindings)) return;
|
||||
if (!ts.isInterfaceDeclaration(node) || !node.name) return;
|
||||
|
||||
for (const specifier of importClause.namedBindings.elements) {
|
||||
if (specifier.name.text === customMediaClassName) {
|
||||
importSource = node.moduleSpecifier.text;
|
||||
break;
|
||||
const name = node.name.text;
|
||||
const extendsList: string[] = [];
|
||||
const keys: string[] = [];
|
||||
|
||||
if (node.heritageClauses) {
|
||||
for (const clause of node.heritageClauses) {
|
||||
if (clause.token !== ts.SyntaxKind.ExtendsKeyword) continue;
|
||||
for (const type of clause.types) {
|
||||
if (ts.isIdentifier(type.expression)) {
|
||||
extendsList.push(type.expression.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Fallback: default to video (all current media elements are video-based)
|
||||
if (!importSource) return 'video';
|
||||
|
||||
const resolvedPath = resolveModuleToFile(mediaFilePath, importSource, compilerOptions);
|
||||
if (!resolvedPath) return 'video';
|
||||
|
||||
const sourceContent = fs.readFileSync(resolvedPath, 'utf-8');
|
||||
const resolvedSourceFile = ts.createSourceFile(resolvedPath, sourceContent, ts.ScriptTarget.Latest, true);
|
||||
|
||||
// Check the extends clause for CustomAudioElement specifically
|
||||
let mediaType: 'video' | 'audio' = 'video';
|
||||
ts.forEachChild(resolvedSourceFile, (node) => {
|
||||
if (!ts.isClassDeclaration(node) || node.name?.text !== customMediaClassName) return;
|
||||
if (!node.heritageClauses) return;
|
||||
|
||||
const extendsClause = node.heritageClauses.find((h) => h.token === ts.SyntaxKind.ExtendsKeyword);
|
||||
if (!extendsClause || extendsClause.types.length === 0) return;
|
||||
|
||||
// Walk the extends expression looking for CustomAudioElement identifier
|
||||
function checkForAudio(n: ts.Node): void {
|
||||
if (ts.isIdentifier(n) && n.text === 'CustomAudioElement') {
|
||||
mediaType = 'audio';
|
||||
return;
|
||||
for (const member of node.members) {
|
||||
if (ts.isPropertySignature(member) && member.name && ts.isIdentifier(member.name)) {
|
||||
keys.push(member.name.text);
|
||||
}
|
||||
ts.forEachChild(n, checkForAudio);
|
||||
}
|
||||
checkForAudio(extendsClause.types[0]!.expression);
|
||||
|
||||
interfaces.set(name, { extends: extendsList, keys });
|
||||
});
|
||||
|
||||
return mediaType;
|
||||
// Recursively collect keys from the target interface and all ancestors
|
||||
const events: string[] = [];
|
||||
const visited = new Set<string>();
|
||||
|
||||
function collect(name: string): void {
|
||||
if (visited.has(name)) return;
|
||||
visited.add(name);
|
||||
|
||||
const iface = interfaces.get(name);
|
||||
if (!iface) return;
|
||||
|
||||
for (const parent of iface.extends) {
|
||||
collect(parent);
|
||||
}
|
||||
events.push(...iface.keys);
|
||||
}
|
||||
|
||||
collect(interfaceName);
|
||||
return events;
|
||||
}
|
||||
|
||||
// ─── Pipeline ────────────────────────────────────────────────────────
|
||||
@@ -527,8 +611,12 @@ export function generateMediaElementReferences(monorepoRoot: string): MediaEleme
|
||||
if (!fs.existsSync(customMediaPath)) return [];
|
||||
|
||||
// Read shared data
|
||||
const allAttributes = extractStringArray(customMediaPath, 'Attributes');
|
||||
const allEvents = extractStringArray(customMediaPath, 'Events');
|
||||
const allAttributes = extractStaticProperties(customMediaPath);
|
||||
|
||||
// Extract events from capability contract types
|
||||
const mediaTypesPath = path.join(monorepoRoot, 'packages/core/src/core/media/types.ts');
|
||||
const videoEvents = fs.existsSync(mediaTypesPath) ? extractEventsFromTypes(mediaTypesPath, 'VideoEvents') : [];
|
||||
const audioEvents = fs.existsSync(mediaTypesPath) ? extractEventsFromTypes(mediaTypesPath, 'AudioEvents') : [];
|
||||
|
||||
// Extract CSS vars using the existing handler (needs a TS program)
|
||||
const program = ts.createProgram([customMediaPath], compilerOptions);
|
||||
@@ -551,35 +639,30 @@ export function generateMediaElementReferences(monorepoRoot: string): MediaEleme
|
||||
|
||||
// Extract slots from template functions
|
||||
const videoSlots = extractSlotsFromTemplate(customMediaPath, 'getVideoTemplateHTML');
|
||||
const audioSlots = extractSlotsFromTemplate(customMediaPath, 'getAudioTemplateHTML');
|
||||
const audioSlots = extractSlotsFromTemplateFactory(customMediaPath, 'getCommonTemplateHTML');
|
||||
|
||||
const results: MediaElementResult[] = [];
|
||||
|
||||
for (const source of sources) {
|
||||
const delegateProperties = extractDelegateProperties(
|
||||
source.delegateFilePath,
|
||||
source.delegateClassName,
|
||||
compilerOptions
|
||||
);
|
||||
const hostProperties = extractHostProperties(source.hostFilePath, source.hostClassName, compilerOptions);
|
||||
|
||||
const mediaType = resolveMediaType(source.mediaFilePath, source.customMediaClassName, compilerOptions);
|
||||
|
||||
// Deduplicate: delegate props that overlap with native Attributes
|
||||
const delegateAttrNames = new Set<string>();
|
||||
for (const propName of Object.keys(delegateProperties)) {
|
||||
delegateAttrNames.add(propName.toLowerCase());
|
||||
// Deduplicate: host props that overlap with native attributes
|
||||
const hostAttrNames = new Set<string>();
|
||||
for (const propName of Object.keys(hostProperties)) {
|
||||
hostAttrNames.add(propName.toLowerCase());
|
||||
}
|
||||
const nativeAttributes = allAttributes.filter((attr) => !delegateAttrNames.has(attr));
|
||||
const nativeAttributes = allAttributes.filter((attr) => !hostAttrNames.has(attr));
|
||||
|
||||
const cssCustomProperties = mediaType === 'video' ? videoCSSVars : audioCSSVars;
|
||||
const slots = mediaType === 'video' ? videoSlots : audioSlots;
|
||||
const cssCustomProperties = source.mediaType === 'video' ? videoCSSVars : audioCSSVars;
|
||||
const slots = source.mediaType === 'video' ? videoSlots : audioSlots;
|
||||
const events = source.mediaType === 'video' ? videoEvents : audioEvents;
|
||||
|
||||
const reference: MediaElementReference = {
|
||||
name: source.className,
|
||||
tagName: source.tagName,
|
||||
delegateProperties,
|
||||
hostProperties,
|
||||
nativeAttributes,
|
||||
events: [...allEvents],
|
||||
events,
|
||||
cssCustomProperties,
|
||||
slots,
|
||||
};
|
||||
|
||||
@@ -575,7 +575,7 @@ export { generatePresetReferences } from './preset-handler.js';
|
||||
// MEDIA ELEMENT REFERENCE PIPELINE
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
export interface DelegatePropertyDef {
|
||||
export interface HostPropertyDef {
|
||||
type: string;
|
||||
description?: string;
|
||||
readonly: boolean;
|
||||
@@ -584,7 +584,7 @@ export interface DelegatePropertyDef {
|
||||
export interface MediaElementReference {
|
||||
name: string;
|
||||
tagName: string;
|
||||
delegateProperties: Record<string, DelegatePropertyDef>;
|
||||
hostProperties: Record<string, HostPropertyDef>;
|
||||
nativeAttributes: string[];
|
||||
events: string[];
|
||||
cssCustomProperties: Record<string, { description: string }>;
|
||||
|
||||
@@ -62,21 +62,21 @@
|
||||
*
|
||||
* Media elements (packages/html/src/define/media/ + packages/core/src/dom/media/):
|
||||
* simple-video — Simple media element. Exercises: discovery via static
|
||||
* tagName in define/media/*.ts, minimal delegate (src rw,
|
||||
* engine readonly), shared Attributes/Events/CSS vars
|
||||
* tagName in define/media/*.ts, minimal host (src rw,
|
||||
* engine readonly), shared attributes/events/CSS vars
|
||||
* from custom-media-element, slots parsed from template HTML.
|
||||
* complex-video — Complex media element. Exercises: delegate with JSDoc
|
||||
* complex-video — Complex media element. Exercises: host with JSDoc
|
||||
* descriptions, multiple property types (string, boolean,
|
||||
* Record), delegate-vs-native attribute deduplication
|
||||
* (src, preload in delegate → omitted from nativeAttributes).
|
||||
* extending-video — Extending media element. Exercises: delegate inheritance
|
||||
* (ExtendingDelegate extends ComplexDelegate). Builder must
|
||||
* Record), host-vs-native attribute deduplication
|
||||
* (src, preload in host → omitted from nativeAttributes).
|
||||
* extending-video — Extending media element. Exercises: host inheritance
|
||||
* (ExtendingHost extends ComplexHost). Builder must
|
||||
* walk the extends chain to include inherited properties.
|
||||
* Child overrides (debug) replace parent definitions.
|
||||
* container.ts — Exclusion case. Not a media element — re-exports an
|
||||
* existing class instead of declaring one inline.
|
||||
* background-video.ts — Exclusion case. Uses MediaAttachMixin(HTMLElement)
|
||||
* without MediaPropsMixin. API reference manually maintained.
|
||||
* without CustomMediaElement. API reference manually maintained.
|
||||
*/
|
||||
import * as path from 'node:path';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
@@ -988,25 +988,25 @@ describe('Preset pipeline (end-to-end)', () => {
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
//
|
||||
// Media elements are custom elements that wrap native <video>/<audio> with
|
||||
// streaming delegates (HLS, DASH, etc.). They are discovered from
|
||||
// streaming hosts (HLS, DASH, etc.). They are discovered from
|
||||
// packages/html/src/define/media/*.ts by looking for files that declare a
|
||||
// class with `static tagName`.
|
||||
//
|
||||
// The builder extracts:
|
||||
// - Tag name from the element class's static tagName
|
||||
// - Delegate properties by following the mixin chain to the delegate class
|
||||
// and walking its getter/setter pairs (mirrors MediaPropsMixin at runtime)
|
||||
// - Shared native attributes, events, and CSS vars from custom-media-element
|
||||
// - Slots parsed from the template HTML (getVideoTemplateHTML / getAudioTemplateHTML)
|
||||
// - JSDoc descriptions from delegate getter/setter pairs
|
||||
// - Host properties by following the CustomMediaElement(tag, Host) call to the
|
||||
// host class and walking its getter/setter pairs
|
||||
// - Shared native attributes from static properties, events, and CSS vars
|
||||
// - Slots parsed from the template HTML (getVideoTemplateHTML / getCommonTemplateHTML)
|
||||
// - JSDoc descriptions from host getter/setter pairs
|
||||
//
|
||||
// Key behaviors:
|
||||
// - Discovery: files in define/media/ with an inline class declaration + static tagName
|
||||
// - Exclusion: container.ts (re-exports, no inline class), background-video.ts
|
||||
// (no MediaPropsMixin — uses MediaAttachMixin(HTMLElement) directly)
|
||||
// - Delegate inheritance: child delegate extends parent, builder walks the chain
|
||||
// - Deduplication: properties in the delegate that overlap with native Attributes
|
||||
// (e.g., src, preload) appear in delegateProperties and are omitted from nativeAttributes
|
||||
// (no CustomMediaElement — uses MediaAttachMixin(HTMLElement) directly)
|
||||
// - Host inheritance: child host extends parent, builder walks the chain
|
||||
// - Deduplication: properties in the host that overlap with native attributes
|
||||
// (e.g., src, preload) appear in hostProperties and are omitted from nativeAttributes
|
||||
|
||||
describe('Media element pipeline (end-to-end)', () => {
|
||||
const results = generateMediaElementReferences(FIXTURE_ROOT);
|
||||
@@ -1030,7 +1030,7 @@ describe('Media element pipeline (end-to-end)', () => {
|
||||
expect(findElement('MediaContainerElement')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('excludes background-video (no MediaPropsMixin, manually maintained)', () => {
|
||||
it('excludes background-video (no CustomMediaElement, manually maintained)', () => {
|
||||
expect(findElement('BackgroundVideo')).toBeUndefined();
|
||||
expect(findElement('BackgroundVideoElement')).toBeUndefined();
|
||||
});
|
||||
@@ -1044,19 +1044,19 @@ describe('Media element pipeline (end-to-end)', () => {
|
||||
// SIMPLE MEDIA ELEMENT: SimpleVideo
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
//
|
||||
// A minimal media element with a simple delegate (src rw, engine readonly).
|
||||
// No JSDoc on delegate properties — descriptions should be undefined.
|
||||
// No overlap between delegate props and native Attributes (engine is not
|
||||
// in Attributes), so nativeAttributes should be the full shared list.
|
||||
// A minimal media element with a simple host (src rw, engine readonly).
|
||||
// No JSDoc on host properties — descriptions should be undefined.
|
||||
// No overlap between host props and native attributes (engine is not
|
||||
// in static properties), so nativeAttributes should be the full shared list.
|
||||
|
||||
describe('SimpleVideo (minimal delegate)', () => {
|
||||
describe('SimpleVideo (minimal host)', () => {
|
||||
it('extracts the tag name', () => {
|
||||
const ref = findElement('SimpleVideo')!.reference;
|
||||
expect(ref.tagName).toBe('simple-video');
|
||||
});
|
||||
|
||||
it('extracts delegate properties with types and readonly flags', () => {
|
||||
const props = findElement('SimpleVideo')!.reference.delegateProperties;
|
||||
it('extracts host properties with types and readonly flags', () => {
|
||||
const props = findElement('SimpleVideo')!.reference.hostProperties;
|
||||
|
||||
// src: read-write string
|
||||
expect(props.src).toMatchObject({
|
||||
@@ -1072,16 +1072,16 @@ describe('Media element pipeline (end-to-end)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('excludes delegate methods (attach, detach, destroy)', () => {
|
||||
const props = findElement('SimpleVideo')!.reference.delegateProperties;
|
||||
it('excludes host lifecycle methods (attach, detach, destroy)', () => {
|
||||
const props = findElement('SimpleVideo')!.reference.hostProperties;
|
||||
expect(props.attach).toBeUndefined();
|
||||
expect(props.detach).toBeUndefined();
|
||||
expect(props.destroy).toBeUndefined();
|
||||
});
|
||||
|
||||
it('includes native attributes from the shared Attributes array', () => {
|
||||
it('includes native attributes from static properties', () => {
|
||||
const ref = findElement('SimpleVideo')!.reference;
|
||||
// src is in the delegate, so it should be omitted from nativeAttributes
|
||||
// src is in the host, so it should be omitted from nativeAttributes
|
||||
expect(ref.nativeAttributes).toEqual(
|
||||
expect.arrayContaining([
|
||||
'autoplay',
|
||||
@@ -1097,20 +1097,35 @@ describe('Media element pipeline (end-to-end)', () => {
|
||||
expect(ref.nativeAttributes).not.toContain('src');
|
||||
});
|
||||
|
||||
it('includes events from the shared Events array', () => {
|
||||
it('includes events derived from VideoEvents capability contracts', () => {
|
||||
const ref = findElement('SimpleVideo')!.reference;
|
||||
expect(ref.events).toEqual(
|
||||
expect.arrayContaining([
|
||||
'abort',
|
||||
'canplay',
|
||||
'durationchange',
|
||||
'ended',
|
||||
'pause',
|
||||
'play',
|
||||
'timeupdate',
|
||||
'volumechange',
|
||||
])
|
||||
);
|
||||
// Events are extracted from VideoEvents in types.ts, which extends
|
||||
// all capability event interfaces including TextTrackListEvents
|
||||
expect(ref.events).toEqual([
|
||||
'play',
|
||||
'playing',
|
||||
'waiting',
|
||||
'pause',
|
||||
'ended',
|
||||
'timeupdate',
|
||||
'durationchange',
|
||||
'seeking',
|
||||
'seeked',
|
||||
'loadedmetadata',
|
||||
'loadstart',
|
||||
'emptied',
|
||||
'canplay',
|
||||
'canplaythrough',
|
||||
'loadeddata',
|
||||
'volumechange',
|
||||
'ratechange',
|
||||
'progress',
|
||||
'error',
|
||||
'addtrack',
|
||||
'removetrack',
|
||||
'changetrack',
|
||||
'trackmodechange',
|
||||
]);
|
||||
});
|
||||
|
||||
it('includes CSS custom properties from VideoCSSVars', () => {
|
||||
@@ -1133,25 +1148,25 @@ describe('Media element pipeline (end-to-end)', () => {
|
||||
// COMPLEX MEDIA ELEMENT: ComplexVideo
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
//
|
||||
// A full media element with a complex delegate that has JSDoc descriptions,
|
||||
// multiple property types, and overlap with native Attributes (src, preload).
|
||||
// A full media element with a complex host that has JSDoc descriptions,
|
||||
// multiple property types, and overlap with native attributes (src, preload).
|
||||
// Tests that the builder extracts descriptions from JSDoc on getters and
|
||||
// deduplicates delegate props from nativeAttributes.
|
||||
// deduplicates host props from nativeAttributes.
|
||||
|
||||
describe('ComplexVideo (full delegate, JSDoc, deduplication)', () => {
|
||||
describe('ComplexVideo (full host, JSDoc, deduplication)', () => {
|
||||
it('extracts the tag name', () => {
|
||||
const ref = findElement('ComplexVideo')!.reference;
|
||||
expect(ref.tagName).toBe('complex-video');
|
||||
});
|
||||
|
||||
it('extracts all delegate properties', () => {
|
||||
const props = findElement('ComplexVideo')!.reference.delegateProperties;
|
||||
it('extracts all host properties', () => {
|
||||
const props = findElement('ComplexVideo')!.reference.hostProperties;
|
||||
const propNames = Object.keys(props).sort();
|
||||
expect(propNames).toEqual(['config', 'debug', 'engine', 'preferPlayback', 'preload', 'src', 'type']);
|
||||
});
|
||||
|
||||
it('extracts JSDoc descriptions from delegate getters', () => {
|
||||
const props = findElement('ComplexVideo')!.reference.delegateProperties;
|
||||
it('extracts JSDoc descriptions from host getters', () => {
|
||||
const props = findElement('ComplexVideo')!.reference.hostProperties;
|
||||
expect(props.type.description).toBe('Explicit source type. When unset, inferred from the source URL extension.');
|
||||
expect(props.preferPlayback.description).toBe("Whether to prefer `'mse'` or `'native'` playback.");
|
||||
expect(props.debug.description).toBe('Enable debug logging.');
|
||||
@@ -1159,7 +1174,7 @@ describe('Media element pipeline (end-to-end)', () => {
|
||||
});
|
||||
|
||||
it('marks readonly properties correctly', () => {
|
||||
const props = findElement('ComplexVideo')!.reference.delegateProperties;
|
||||
const props = findElement('ComplexVideo')!.reference.hostProperties;
|
||||
// engine: getter only → readonly
|
||||
expect(props.engine.readonly).toBe(true);
|
||||
// src: getter + setter → not readonly
|
||||
@@ -1168,18 +1183,18 @@ describe('Media element pipeline (end-to-end)', () => {
|
||||
});
|
||||
|
||||
it('extracts property types', () => {
|
||||
const props = findElement('ComplexVideo')!.reference.delegateProperties;
|
||||
const props = findElement('ComplexVideo')!.reference.hostProperties;
|
||||
expect(props.src.type).toBe('string');
|
||||
expect(props.debug.type).toBe('boolean');
|
||||
expect(props.config.type).toContain('Record');
|
||||
});
|
||||
|
||||
it('deduplicates delegate props from nativeAttributes', () => {
|
||||
it('deduplicates host props from nativeAttributes', () => {
|
||||
const ref = findElement('ComplexVideo')!.reference;
|
||||
// src and preload are in both the delegate AND native Attributes.
|
||||
// They should appear in delegateProperties...
|
||||
expect(ref.delegateProperties.src).toBeDefined();
|
||||
expect(ref.delegateProperties.preload).toBeDefined();
|
||||
// src and preload are in both the host AND native attributes.
|
||||
// They should appear in hostProperties...
|
||||
expect(ref.hostProperties.src).toBeDefined();
|
||||
expect(ref.hostProperties.preload).toBeDefined();
|
||||
// ...and be omitted from nativeAttributes
|
||||
expect(ref.nativeAttributes).not.toContain('src');
|
||||
expect(ref.nativeAttributes).not.toContain('preload');
|
||||
@@ -1193,19 +1208,19 @@ describe('Media element pipeline (end-to-end)', () => {
|
||||
// EXTENDING MEDIA ELEMENT: ExtendingVideo
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
//
|
||||
// A media element whose delegate extends another delegate (mirrors
|
||||
// MuxMediaBase extending HlsMediaBase). The builder must
|
||||
// walk the extends chain to include inherited properties. Child
|
||||
// properties override parent definitions.
|
||||
// A media element whose host extends another host (mirrors
|
||||
// MuxVideoMedia extending HlsMedia). The builder must walk the
|
||||
// extends chain to include inherited properties. Child properties
|
||||
// override parent definitions.
|
||||
|
||||
describe('ExtendingVideo (delegate inheritance)', () => {
|
||||
describe('ExtendingVideo (host inheritance)', () => {
|
||||
it('extracts the tag name', () => {
|
||||
const ref = findElement('ExtendingVideo')!.reference;
|
||||
expect(ref.tagName).toBe('extending-video');
|
||||
});
|
||||
|
||||
it('includes own properties from ExtendingDelegate', () => {
|
||||
const props = findElement('ExtendingVideo')!.reference.delegateProperties;
|
||||
it('includes own properties from ExtendingHost', () => {
|
||||
const props = findElement('ExtendingVideo')!.reference.hostProperties;
|
||||
expect(props.playbackId).toMatchObject({
|
||||
type: 'string',
|
||||
readonly: false,
|
||||
@@ -1218,9 +1233,9 @@ describe('Media element pipeline (end-to-end)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('includes inherited properties from ComplexDelegate', () => {
|
||||
const props = findElement('ExtendingVideo')!.reference.delegateProperties;
|
||||
// These are inherited from ComplexDelegate
|
||||
it('includes inherited properties from ComplexHost', () => {
|
||||
const props = findElement('ExtendingVideo')!.reference.hostProperties;
|
||||
// These are inherited from ComplexHost
|
||||
expect(props.src).toBeDefined();
|
||||
expect(props.type).toBeDefined();
|
||||
expect(props.preferPlayback).toBeDefined();
|
||||
@@ -1230,15 +1245,41 @@ describe('Media element pipeline (end-to-end)', () => {
|
||||
});
|
||||
|
||||
it('child overrides replace parent definitions', () => {
|
||||
const props = findElement('ExtendingVideo')!.reference.delegateProperties;
|
||||
// ExtendingDelegate overrides debug with different JSDoc
|
||||
const props = findElement('ExtendingVideo')!.reference.hostProperties;
|
||||
// ExtendingHost overrides debug with different JSDoc
|
||||
expect(props.debug.description).toBe('Overrides parent debug — adds network logging.');
|
||||
});
|
||||
|
||||
it('inherited readonly flags are preserved', () => {
|
||||
const props = findElement('ExtendingVideo')!.reference.delegateProperties;
|
||||
// engine is readonly in ComplexDelegate and not overridden
|
||||
const props = findElement('ExtendingVideo')!.reference.hostProperties;
|
||||
// engine is readonly in ComplexHost and not overridden
|
||||
expect(props.engine.readonly).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
// CROSS-CUTTING: EVENT EXTRACTION
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
//
|
||||
// Events are derived from the capability contract types in
|
||||
// packages/core/src/core/media/types.ts, not hardcoded.
|
||||
// VideoEvents includes TextTrackListEvents; AudioEvents does not.
|
||||
|
||||
describe('Event extraction from capability contracts', () => {
|
||||
it('video elements include text track events from VideoEvents', () => {
|
||||
const ref = findElement('SimpleVideo')!.reference;
|
||||
expect(ref.events).toContain('addtrack');
|
||||
expect(ref.events).toContain('removetrack');
|
||||
expect(ref.events).toContain('changetrack');
|
||||
expect(ref.events).toContain('trackmodechange');
|
||||
});
|
||||
|
||||
it('all video elements share the same event list', () => {
|
||||
const simple = findElement('SimpleVideo')!.reference.events;
|
||||
const complex = findElement('ComplexVideo')!.reference.events;
|
||||
const extending = findElement('ExtendingVideo')!.reference.events;
|
||||
expect(complex).toEqual(simple);
|
||||
expect(extending).toEqual(simple);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Vendored
+84
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Mock media contract types — mirrors packages/core/src/core/media/types.ts.
|
||||
*
|
||||
* Exercises: event extraction from capability event interfaces.
|
||||
* VideoEvents extends all capability events (including TextTrackListEvents).
|
||||
* AudioEvents extends a subset (no text track events).
|
||||
*/
|
||||
|
||||
export interface EventLike<Detail = void> {
|
||||
readonly type: string;
|
||||
readonly timeStamp: number;
|
||||
readonly detail?: Detail;
|
||||
}
|
||||
|
||||
export interface MediaPlaybackEvents {
|
||||
play: EventLike;
|
||||
playing: EventLike;
|
||||
waiting: EventLike;
|
||||
}
|
||||
|
||||
export interface MediaPauseEvents {
|
||||
pause: EventLike;
|
||||
ended: EventLike;
|
||||
}
|
||||
|
||||
export interface MediaSeekEvents {
|
||||
timeupdate: EventLike;
|
||||
durationchange: EventLike;
|
||||
seeking: EventLike;
|
||||
seeked: EventLike;
|
||||
loadedmetadata: EventLike;
|
||||
}
|
||||
|
||||
export interface MediaSourceEvents {
|
||||
loadstart: EventLike;
|
||||
emptied: EventLike;
|
||||
canplay: EventLike;
|
||||
canplaythrough: EventLike;
|
||||
loadeddata: EventLike;
|
||||
}
|
||||
|
||||
export interface MediaVolumeEvents {
|
||||
volumechange: EventLike;
|
||||
}
|
||||
|
||||
export interface MediaPlaybackRateEvents {
|
||||
ratechange: EventLike;
|
||||
}
|
||||
|
||||
export interface MediaBufferEvents {
|
||||
progress: EventLike;
|
||||
}
|
||||
|
||||
export interface MediaErrorEvents {
|
||||
error: EventLike;
|
||||
}
|
||||
|
||||
export interface TextTrackListEvents {
|
||||
addtrack: EventLike;
|
||||
removetrack: EventLike;
|
||||
changetrack: EventLike;
|
||||
trackmodechange: EventLike;
|
||||
}
|
||||
|
||||
export interface VideoEvents
|
||||
extends MediaPlaybackEvents,
|
||||
MediaPauseEvents,
|
||||
MediaSeekEvents,
|
||||
MediaSourceEvents,
|
||||
MediaVolumeEvents,
|
||||
MediaPlaybackRateEvents,
|
||||
MediaBufferEvents,
|
||||
MediaErrorEvents,
|
||||
TextTrackListEvents {}
|
||||
|
||||
export interface AudioEvents
|
||||
extends MediaPlaybackEvents,
|
||||
MediaPauseEvents,
|
||||
MediaSeekEvents,
|
||||
MediaSourceEvents,
|
||||
MediaVolumeEvents,
|
||||
MediaPlaybackRateEvents,
|
||||
MediaBufferEvents,
|
||||
MediaErrorEvents {}
|
||||
+5
-9
@@ -1,11 +1,13 @@
|
||||
/**
|
||||
* Mock complex delegate — mirrors HlsMediaBase.
|
||||
* Mock complex host — mirrors HlsMedia.
|
||||
*
|
||||
* Exercises: multiple getter/setter pairs with JSDoc descriptions,
|
||||
* readonly properties, boolean type, overlap with native Attributes
|
||||
* readonly properties, boolean type, overlap with native attributes
|
||||
* (src, preload) that should be deduplicated by the builder.
|
||||
*/
|
||||
export class ComplexDelegate {
|
||||
import { HTMLVideoElementHost } from '../simple';
|
||||
|
||||
export class ComplexHost extends HTMLVideoElementHost {
|
||||
#src: string = '';
|
||||
#type: string | undefined;
|
||||
#preferPlayback: string | undefined = 'mse';
|
||||
@@ -69,10 +71,4 @@ export class ComplexDelegate {
|
||||
get engine(): object | null {
|
||||
return this.#engine;
|
||||
}
|
||||
|
||||
attach(_target: EventTarget): void {}
|
||||
detach(): void {}
|
||||
destroy(): void {}
|
||||
}
|
||||
|
||||
export class ComplexCustomMedia {}
|
||||
|
||||
+41
-46
@@ -1,37 +1,14 @@
|
||||
/**
|
||||
* Mock custom media element infrastructure.
|
||||
*
|
||||
* Exercises: shared Events, Attributes, and CSS vars that the builder reads
|
||||
* to populate media element references. Slots are parsed from the template
|
||||
* HTML (getVideoTemplateHTML / getAudioTemplateHTML), not from exported arrays.
|
||||
* Exercises: shared native attributes (via static properties), CSS vars,
|
||||
* and slots that the builder reads to populate media element references.
|
||||
* Slots are parsed from getVideoTemplateHTML / getCommonTemplateHTML.
|
||||
*
|
||||
* VideoCSSVars/AudioCSSVars follow the `{ camelKey: '--var-name' }` pattern
|
||||
* with JSDoc descriptions, matching UI component css-vars files.
|
||||
*/
|
||||
|
||||
export const Events = [
|
||||
'abort',
|
||||
'canplay',
|
||||
'durationchange',
|
||||
'ended',
|
||||
'pause',
|
||||
'play',
|
||||
'timeupdate',
|
||||
'volumechange',
|
||||
] as const;
|
||||
|
||||
export const Attributes = [
|
||||
'autoplay',
|
||||
'controls',
|
||||
'crossorigin',
|
||||
'loop',
|
||||
'muted',
|
||||
'playsinline',
|
||||
'poster',
|
||||
'preload',
|
||||
'src',
|
||||
] as const;
|
||||
|
||||
/** CSS custom property names for video elements. */
|
||||
export const VideoCSSVars = {
|
||||
/** Border radius of the video element. */
|
||||
@@ -68,26 +45,44 @@ function getVideoTemplateHTML(attrs: Record<string, string>): string {
|
||||
`;
|
||||
}
|
||||
|
||||
function getAudioTemplateHTML(attrs: Record<string, string>): string {
|
||||
return /*html*/ `
|
||||
<style>
|
||||
audio { width: 100%; }
|
||||
</style>
|
||||
<slot name="media">
|
||||
<audio></audio>
|
||||
</slot>
|
||||
<slot></slot>
|
||||
`;
|
||||
function getCommonTemplateHTML(tag: string) {
|
||||
return (attrs: Record<string, string>) => {
|
||||
return /*html*/ `
|
||||
<style>
|
||||
${tag} { width: 100%; }
|
||||
</style>
|
||||
<slot name="media">
|
||||
<${tag}></${tag}>
|
||||
</slot>
|
||||
<slot></slot>
|
||||
`;
|
||||
};
|
||||
}
|
||||
|
||||
// Minimal stubs — the builder only needs to detect these by name, not run them.
|
||||
export function CustomMediaMixin(base: any, _opts: any) {
|
||||
return base;
|
||||
}
|
||||
// Stub — the builder parses the AST, it doesn't run the code.
|
||||
// Mirrors the real CustomMediaElement factory signature.
|
||||
export function CustomMediaElement(tag: string, Host: any) {
|
||||
class CustomMedia {
|
||||
static getTemplateHTML = tag === 'video' ? getVideoTemplateHTML : getCommonTemplateHTML(tag);
|
||||
static shadowRootOptions = { mode: 'open' };
|
||||
|
||||
export const CustomVideoElement = class {
|
||||
static getTemplateHTML = getVideoTemplateHTML;
|
||||
};
|
||||
export const CustomAudioElement = class {
|
||||
static getTemplateHTML = getAudioTemplateHTML;
|
||||
};
|
||||
static properties = {
|
||||
autoPictureInPicture: { type: Boolean },
|
||||
autoplay: { type: Boolean },
|
||||
controls: { type: Boolean },
|
||||
controlsList: { type: String },
|
||||
crossOrigin: { type: String },
|
||||
defaultMuted: { type: Boolean, attribute: 'muted' },
|
||||
disablePictureInPicture: { type: Boolean },
|
||||
disableRemotePlayback: { type: Boolean },
|
||||
loading: { type: String },
|
||||
loop: { type: Boolean },
|
||||
playsInline: { type: Boolean },
|
||||
poster: { type: String },
|
||||
preload: { type: String },
|
||||
src: { type: String },
|
||||
};
|
||||
}
|
||||
|
||||
return CustomMedia;
|
||||
}
|
||||
|
||||
+5
-7
@@ -1,13 +1,13 @@
|
||||
/**
|
||||
* Mock extending delegate — mirrors MuxMediaBase extending HlsMediaBase.
|
||||
* Mock extending host — mirrors MuxVideoMedia extending HlsMedia.
|
||||
*
|
||||
* Exercises: delegate inheritance. The builder must walk the extends chain
|
||||
* to extract properties from both this class and its parent (ComplexDelegate).
|
||||
* Exercises: host inheritance. The builder must walk the extends chain
|
||||
* to extract properties from both this class and its parent (ComplexHost).
|
||||
* Child properties override parent properties of the same name.
|
||||
*/
|
||||
import { ComplexDelegate } from '../complex';
|
||||
import { ComplexHost } from '../complex';
|
||||
|
||||
export class ExtendingDelegate extends ComplexDelegate {
|
||||
export class ExtendingHost extends ComplexHost {
|
||||
#playbackId: string = '';
|
||||
#customDomain: string = '';
|
||||
|
||||
@@ -38,5 +38,3 @@ export class ExtendingDelegate extends ComplexDelegate {
|
||||
super.debug = value;
|
||||
}
|
||||
}
|
||||
|
||||
export class ExtendingCustomMedia {}
|
||||
|
||||
+11
-9
@@ -1,10 +1,18 @@
|
||||
/**
|
||||
* Mock simple delegate — mirrors DashMediaBase.
|
||||
* Mock simple host — mirrors DashMedia.
|
||||
*
|
||||
* Exercises: minimal delegate with just src (read-write) and engine (readonly).
|
||||
* Exercises: minimal host with just src (read-write) and engine (readonly).
|
||||
* No JSDoc on properties — tests that missing descriptions produce undefined.
|
||||
*/
|
||||
export class SimpleDelegate {
|
||||
|
||||
// Stub — the builder walks the prototype chain and stops here.
|
||||
export class HTMLVideoElementHost {
|
||||
attach(_target: EventTarget): void {}
|
||||
detach(): void {}
|
||||
destroy(): void {}
|
||||
}
|
||||
|
||||
export class SimpleHost extends HTMLVideoElementHost {
|
||||
#src: string = '';
|
||||
#engine: object = {};
|
||||
|
||||
@@ -19,10 +27,4 @@ export class SimpleDelegate {
|
||||
get engine(): object {
|
||||
return this.#engine;
|
||||
}
|
||||
|
||||
attach(_target: EventTarget): void {}
|
||||
detach(): void {}
|
||||
destroy(): void {}
|
||||
}
|
||||
|
||||
export class SimpleCustomMedia {}
|
||||
|
||||
+7
-8
@@ -1,17 +1,16 @@
|
||||
/**
|
||||
* Mock complex media element — mirrors HlsVideo.
|
||||
*
|
||||
* Exercises: standard mixin composition with a complex delegate
|
||||
* that has JSDoc descriptions on its getter/setters.
|
||||
* Exercises: standard composition with CustomMediaElement factory
|
||||
* and a complex host that has JSDoc descriptions on its getter/setters.
|
||||
*/
|
||||
import { ComplexCustomMedia, ComplexDelegate } from '../../../../core/src/dom/media/complex';
|
||||
|
||||
// Stubs — the builder parses the AST, it doesn't run the code.
|
||||
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;
|
||||
}
|
||||
function MediaPropsMixin(base: any, _delegate: any) {
|
||||
return base;
|
||||
}
|
||||
|
||||
export class ComplexVideo extends MediaPropsMixin(MediaAttachMixin(ComplexCustomMedia), ComplexDelegate) {}
|
||||
export class ComplexVideo extends MediaAttachMixin(CustomMediaElement('video', ComplexHost)) {}
|
||||
|
||||
+4
-6
@@ -1,15 +1,13 @@
|
||||
/**
|
||||
* Mock extending media element — mirrors MuxVideo.
|
||||
*
|
||||
* Exercises: media element with a delegate that inherits from another delegate.
|
||||
* Exercises: media element with a host that inherits from another host.
|
||||
*/
|
||||
import { ExtendingCustomMedia, ExtendingDelegate } from '../../../../core/src/dom/media/extending';
|
||||
import { CustomMediaElement } from '../../../../core/src/dom/media/custom-media-element';
|
||||
import { ExtendingHost } from '../../../../core/src/dom/media/extending';
|
||||
|
||||
function MediaAttachMixin(base: any) {
|
||||
return base;
|
||||
}
|
||||
function MediaPropsMixin(base: any, _delegate: any) {
|
||||
return base;
|
||||
}
|
||||
|
||||
export class ExtendingVideo extends MediaPropsMixin(MediaAttachMixin(ExtendingCustomMedia), ExtendingDelegate) {}
|
||||
export class ExtendingVideo extends MediaAttachMixin(CustomMediaElement('video', ExtendingHost)) {}
|
||||
|
||||
+6
-8
@@ -1,18 +1,16 @@
|
||||
/**
|
||||
* Mock simple media element — mirrors DashVideo.
|
||||
*
|
||||
* Exercises: standard mixin composition with a simple delegate.
|
||||
* The builder follows this import chain to discover the delegate class
|
||||
* Exercises: standard composition with CustomMediaElement factory.
|
||||
* The builder follows this import chain to discover the host class
|
||||
* and resolve its properties.
|
||||
*/
|
||||
import { SimpleCustomMedia, SimpleDelegate } from '../../../../core/src/dom/media/simple';
|
||||
import { CustomMediaElement } from '../../../../core/src/dom/media/custom-media-element';
|
||||
import { SimpleHost } from '../../../../core/src/dom/media/simple';
|
||||
|
||||
// Stubs — the builder parses the AST, it doesn't run the code.
|
||||
// Stub — the builder parses the AST, it doesn't run the code.
|
||||
function MediaAttachMixin(base: any) {
|
||||
return base;
|
||||
}
|
||||
function MediaPropsMixin(base: any, _delegate: any) {
|
||||
return base;
|
||||
}
|
||||
|
||||
export class SimpleVideo extends MediaPropsMixin(MediaAttachMixin(SimpleCustomMedia), SimpleDelegate) {}
|
||||
export class SimpleVideo extends MediaAttachMixin(CustomMediaElement('video', SimpleHost)) {}
|
||||
|
||||
Reference in New Issue
Block a user